diff --git a/.cursor/environment.json b/.cursor/environment.json deleted file mode 100644 index f32303afa..000000000 --- a/.cursor/environment.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "terminals": [] -} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index 7d2f6b9d0..000000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Autumn dev container", - "image": "mcr.microsoft.com/devcontainers/node:18", - "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", - "forwardPorts": [8080], - // "postCreateCommand": "pnpm install", - "postCreateCommand": "apt update && apt install -y zsh", - "settings": { - "terminal.integrated.shell.linux": "/bin/zsh" - }, - "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] -} diff --git a/AGENTS.md b/AGENTS.md index 5d03c3369..3fc38f0ae 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,36 @@ - Functions (unless there's a very good reason) should always take in objects as arguments. Object params are named and easy to understand. +## Error Handling in API Routes +- NEVER use `c.json({ message: "...", code: "..." }, statusCode)` pattern for input validation or expected errors in Hono routes +- ALWAYS throw `RecaseError` from `@autumn/shared` for all validation errors, not found errors, forbidden errors, etc. +- For internal/unexpected errors (like missing configuration, database errors, etc.), throw `InternalError` from `@autumn/shared` +- The onError middleware automatically converts these errors to appropriate HTTP responses +- Examples: + ```typescript + // ❌ BAD - Don't do this + if (!org) { + return c.json({ message: "Org not found", code: "not_found" }, 404); + } + + // ✅ GOOD - Validation/expected errors use RecaseError + if (!org) { + throw new RecaseError({ + message: "Org not found", + code: ErrCode.NotFound, + statusCode: 404, + }); + } + + // ✅ GOOD - Internal/unexpected errors use InternalError + if (!upstash) { + throw new InternalError({ + message: "Upstash not configured", + code: "upstash_not_configured", + }); + } + ``` + ## Bad example / root -> components diff --git a/CLAUDE.md b/CLAUDE.md index fbe69850d..357e9870a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,15 +6,53 @@ - When using db schemas in Drizzle, import them from '@autumn/shared', and don't do schemas. # Linting and Codebase rules -- You can access the biome linter by running `npx biome check `. Always specify a folder path, as the codebase is quite large and you will get out of scope errors that you are not burdened to correct. If you would like to let biome automatically fix as much as it can, use `npx biome check --write ` +- You can access the biome linter by running `bunx biome check `. Always specify a folder path, as the codebase is quite large and you will get out of scope errors that you are not burdened to correct. If you would like to let biome automatically fix as much as it can, use `bunx biome check --write ` - Note, biome does not perform typechecking. In which case you need to, you may run `tsc --noEmit --skipLibCheck ` - This codebase uses Bun as its preferred package manager and Node runtime. +- **ALWAYS import from `zod/v4`**, not from `zod` directly. Example: `import { z } from "zod/v4";` + - Always prefer foo({ bar }) over foo(bar) method signatures - no matter if we are using only one argument or not, object as param are always better, as in the future when wanting to change the order of parameters, or add new ones - its easier. - When creating "hooks" folders, don't nest them under "components" + +- Functions (unless there's a very good reason) should always take in objects as arguments. Object params are named and easy to understand. + +- Prefer Guard clauses "if(!admin) return;" over explicity "if(admin) do X;" Early returns are better + +- Do not run "npx tsc" - run "tsc" instead as it is installed globally. +## Error Handling in API Routes +- NEVER use `c.json({ message: "...", code: "..." }, statusCode)` pattern for input validation or expected errors in Hono routes +- ALWAYS throw `RecaseError` from `@autumn/shared` for all validation errors, not found errors, forbidden errors, etc. +- For internal/unexpected errors (like missing configuration, database errors, etc.), throw `InternalError` from `@autumn/shared` +- The onError middleware automatically converts these errors to appropriate HTTP responses +- Examples: + ```typescript + // ❌ BAD - Don't do this + if (!org) { + return c.json({ message: "Org not found", code: "not_found" }, 404); + } + + // ✅ GOOD - Validation/expected errors use RecaseError + if (!org) { + throw new RecaseError({ + message: "Org not found", + code: ErrCode.NotFound, + statusCode: 404, + }); + } + + // ✅ GOOD - Internal/unexpected errors use InternalError + if (!upstash) { + throw new InternalError({ + message: "Upstash not configured", + code: "upstash_not_configured", + }); + } + ``` + ## Bad example / root -> components diff --git a/archives/benchmark.yml b/archives/benchmark.yml deleted file mode 100644 index 4dd9e35e9..000000000 --- a/archives/benchmark.yml +++ /dev/null @@ -1,79 +0,0 @@ -# name: Benchmark Server PR - -# on: -# pull_request: -# types: [opened, synchronize, reopened] -# paths: -# - 'server/**' -# - 'pnpm-lock.yaml' -# - 'package.json' - -# permissions: -# contents: read -# pull-requests: write - -# jobs: -# benchmark: -# runs-on: ubuntu-latest - -# steps: -# - name: Checkout code -# uses: actions/checkout@v4 - -# - name: Setup pnpm -# uses: pnpm/action-setup@v2 -# with: -# version: latest - -# - name: Setup Node.js -# uses: actions/setup-node@v4 -# with: -# node-version: '20' -# cache: 'pnpm' - -# - name: Install dependencies -# run: pnpm i --no-frozen-lockfile - -# - name: Run benchmark -# id: benchmark -# working-directory: ./server -# run: | -# echo "BENCHMARK_OUTPUT<> $GITHUB_OUTPUT -# FULL_OUTPUT=$(pnpm run benchmark 2>&1) -# FILTERED_OUTPUT=$(echo "$FULL_OUTPUT" | grep -v "^> @.*benchmark" | grep -v "^> tsx benchmarks" | grep -v "Benchmark completed successfully") -# echo "$FILTERED_OUTPUT" >> $GITHUB_OUTPUT -# echo "EOF" >> $GITHUB_OUTPUT - -# if echo "$FULL_OUTPUT" | grep -q "Benchmark completed successfully"; then -# echo "BENCHMARK_STATUS=✅ Passed" >> $GITHUB_OUTPUT -# else -# echo "BENCHMARK_STATUS=❌ Failed" >> $GITHUB_OUTPUT -# fi - -# - name: Comment PR -# uses: actions/github-script@v7 -# with: -# script: | -# const output = `${{ steps.benchmark.outputs.BENCHMARK_OUTPUT }}`; -# const status = `${{ steps.benchmark.outputs.BENCHMARK_STATUS }}`; -# const body = `## 📊 Benchmark Results - -# **Benchmark CI:** ${status} - -#
-# Click to view benchmark results - -# \`\`\`javascript -# ${output} -# \`\`\` - -#
- -# *Benchmark run for commit ${{ github.sha }}*`; - -# github.rest.issues.createComment({ -# issue_number: context.issue.number, -# owner: context.repo.owner, -# repo: context.repo.repo, -# body: body -# }); \ No newline at end of file diff --git a/bun.lock b/bun.lock index bf4029070..43e9c7ab7 100644 --- a/bun.lock +++ b/bun.lock @@ -18,11 +18,27 @@ "inquirer": "^12.6.3", }, }, + "scripts": { + "name": "@autumn/scripts", + "version": "1.0.0", + "dependencies": { + "@autumn/shared": "workspace:*", + "chalk": "^5.3.0", + "dotenv": "^16.5.0", + "inquirer": "^12.6.3", + }, + "devDependencies": { + "@types/inquirer": "^9.0.7", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + }, + }, "server": { "name": "@autumn/server", "version": "1.0.0", "dependencies": { "@ai-sdk/anthropic": "^1.2.10", + "@amplitude/analytics-node": "^1.5.18", "@anthropic-ai/sdk": "^0.32.1", "@autumn/shared": "workspace:*", "@axiomhq/pino": "^1.3.1", @@ -94,7 +110,7 @@ "recaseai": "^0.0.37", "resend": "^4.1.1", "semver": "^7.7.2", - "stripe": "^18.4.0", + "stripe": "18.4.0-beta.2", "svix": "^1.45.1", "tsc-alias": "^1.8.16", "ws": "^8.18.0", @@ -154,8 +170,10 @@ "name": "@autumn/vite", "version": "0.0.0", "dependencies": { + "@amplitude/unified": "^1.0.0-beta.9", "@autumn/shared": "workspace:*", "@better-auth/stripe": "^1.2.12", + "@date-fns/utc": "^2.1.1", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", "@heroicons/react": "^2.2.0", @@ -257,8 +275,69 @@ "@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="], +<<<<<<< HEAD +======= + "@amplitude/analytics-browser": ["@amplitude/analytics-browser@2.27.0", "", { "dependencies": { "@amplitude/analytics-core": "^2.28.0", "@amplitude/plugin-autocapture-browser": "^1.15.3", "@amplitude/plugin-network-capture-browser": "^1.6.9", "@amplitude/plugin-page-view-tracking-browser": "^2.5.3", "@amplitude/plugin-web-vitals-browser": "^0.1.0-beta.31", "tslib": "^2.4.1" } }, "sha512-1LBCLmnr7aUpLtOp64lpr8GzN3vPKM0fwiM/7tWJ9XU9/GKA+k3CUSjI8OdERKrw2yVywujoAVQo4anGZXYIDA=="], + + "@amplitude/analytics-client-common": ["@amplitude/analytics-client-common@2.4.8", "", { "dependencies": { "@amplitude/analytics-connector": "^1.4.8", "@amplitude/analytics-core": "^2.28.0", "@amplitude/analytics-types": "^2.10.0", "tslib": "^2.4.1" } }, "sha512-cSm9Q+qcLy65kV2MnD7WlKrFMDOkj14dHM2YQn5njUrSXQljyjtYCss+HVzgIlWPSDgA5v6dDeHwxzZl3VSggw=="], + + "@amplitude/analytics-connector": ["@amplitude/analytics-connector@1.6.4", "", {}, "sha512-SpIv0IQMNIq6SH3UqFGiaZyGSc7PBZwRdq7lvP0pBxW8i4Ny+8zwI0pV+VMfMHQwWY3wdIbWw5WQphNjpdq1/Q=="], + + "@amplitude/analytics-core": ["@amplitude/analytics-core@2.28.0", "", { "dependencies": { "@amplitude/analytics-connector": "^1.6.4", "tslib": "^2.4.1" } }, "sha512-Wj/xUHhiHk2xH0/lp5IgHlzyKJOIb/WkpWP8W66wf3EaLJwX0AtPWMEb557BHbYBG/KXonp6ob9DyD0xbW38dg=="], + + "@amplitude/analytics-node": ["@amplitude/analytics-node@1.5.18", "", { "dependencies": { "@amplitude/analytics-core": "^2.28.0", "tslib": "^2.4.1" } }, "sha512-ZGMAfrIL8znbb1+KHSTaNL+rAo63OdGIRTO35LcZhRf9svNn59Tn+L0vLucp2+TkY/Xo+ILY++Lz7oVLkCj4Xw=="], + + "@amplitude/analytics-remote-config": ["@amplitude/analytics-remote-config@0.6.3", "", { "dependencies": { "@amplitude/analytics-core": ">=1 <2", "@amplitude/analytics-types": ">=1 <2", "tslib": "^2.4.1" } }, "sha512-icE0ogCzdHAtQi9jiOFQUmKrvWQc5YEO6bLZUfQXCT/yTTNXppWnT1zHMKzXa3SMDosfrLwU/X8sro1PTI+jZQ=="], + + "@amplitude/analytics-types": ["@amplitude/analytics-types@1.4.0", "", {}, "sha512-RiMPHBqdrJ8ktTqG+Wzj2htnN/PCG9jGZG0SXtTFnWwVvcAJYbYm55/nrP1TTyrx1OlLhvF2VG3lVUP/xGAU8w=="], + + "@amplitude/engagement-browser": ["@amplitude/engagement-browser@1.0.4", "", { "dependencies": { "@amplitude/analytics-types": "^1.0.0" } }, "sha512-jqLGjONikz/G5J4QxFCdzcB6TBFmNz4cdwFiiJsyShy/hvNs7XqsEf9eQgRH150DT9aPoTccoBeCC+fes72BPw=="], + + "@amplitude/experiment-core": ["@amplitude/experiment-core@0.11.0", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-egqb/eWFUU+gn6w3t9/L8PHpivAZrIVOX6dTk0NUVNfG3jeN4VuB77BOvu51xxfNF3Hvs6J49do9hRtE/LdvSw=="], + + "@amplitude/experiment-js-client": ["@amplitude/experiment-js-client@1.17.1", "", { "dependencies": { "@amplitude/analytics-connector": "^1.6.4", "@amplitude/experiment-core": "^0.11.0", "@amplitude/ua-parser-js": "^0.7.31", "base64-js": "1.5.1", "unfetch": "4.1.0" } }, "sha512-3/+V+elOLui0Lqmfy+ZobICvC2Yli/Nl6eBR00oUHoMo7OABekRIj/MLeRNvHiw38JMP2lAzyRArwvbvUgkGew=="], + + "@amplitude/plugin-autocapture-browser": ["@amplitude/plugin-autocapture-browser@1.15.3", "", { "dependencies": { "@amplitude/analytics-core": "^2.28.0", "rxjs": "^7.8.1", "tslib": "^2.4.1" } }, "sha512-WPYw81fFdTzUxEzM3/lTalsxLNGDKFklGnDaHoLFB4LxL5XfIyC+lBnOls+9zjt7dyV0qvq5YzunbFNT2ysR8g=="], + + "@amplitude/plugin-experiment-browser": ["@amplitude/plugin-experiment-browser@1.0.0-beta.0", "", { "dependencies": { "@amplitude/analytics-core": "^2.10.0", "@amplitude/experiment-js-client": "^1.15.5" } }, "sha512-lJKPxrjHfBzA+3XMDRFrP2wr4eyjFgdaylWuN50+sP+OWuaIBWzWU1lZYjcfXOGyTSf7g/9c5jwDM9A5bY0yHQ=="], + + "@amplitude/plugin-network-capture-browser": ["@amplitude/plugin-network-capture-browser@1.6.9", "", { "dependencies": { "@amplitude/analytics-core": "^2.28.0", "rxjs": "^7.8.1", "tslib": "^2.4.1" } }, "sha512-dBp0FiXGwreFfEZvWBqe+VbbwSRsljRwdYdtSQvBeYriBWZp7g3rD02xmt8zjlWxMpQTAE0wLjAi7E01NclBXg=="], + + "@amplitude/plugin-page-view-tracking-browser": ["@amplitude/plugin-page-view-tracking-browser@2.5.3", "", { "dependencies": { "@amplitude/analytics-core": "^2.28.0", "tslib": "^2.4.1" } }, "sha512-zqdZ01mScGHwvxoCLiz+qClA7sbryozeX2BmRt4mxkk9qkukKuMs2xGijgUDqsrWX0UbkYczFZhPbhm+4Jti9g=="], + + "@amplitude/plugin-session-replay-browser": ["@amplitude/plugin-session-replay-browser@1.22.25", "", { "dependencies": { "@amplitude/analytics-client-common": "^2.4.8", "@amplitude/analytics-core": "^2.28.0", "@amplitude/analytics-types": "^2.10.0", "@amplitude/session-replay-browser": "^1.28.21", "idb-keyval": "^6.2.1", "tslib": "^2.4.1" } }, "sha512-mQmheXS/X+p2SNkuM9knatyiiH5OWExp4WiNhbgzKpiqwSlrDYpuAcvZPmCe0pvTu0lvi2uCP491vor++Ew8bg=="], + + "@amplitude/plugin-web-vitals-browser": ["@amplitude/plugin-web-vitals-browser@0.1.0-frustrationanalytics.0", "", { "dependencies": { "@amplitude/analytics-core": "^2.14.0-frustrationanalytics.0", "rxjs": "^7.8.1", "tslib": "^2.4.1", "web-vitals": "^5.0.1" } }, "sha512-xv4sje6/D8r+SgNFTA22FJ5PhtdhN+VSydvs63Frll+qWlyQwaZ1IgDbPyqjzryEkldHRPD7GUaQual+geoIYg=="], + + "@amplitude/rrdom": ["@amplitude/rrdom@2.0.0-alpha.33", "", { "dependencies": { "@amplitude/rrweb-snapshot": "^2.0.0-alpha.33" } }, "sha512-uu+1w1RGEJ7QcGPwCC898YBR47DpNYOZTnQMY9/IgMzTXQ0+Hh1/JLsQfMnBBtAePhvCS0BlHd/qGD5w0taIcg=="], + + "@amplitude/rrweb": ["@amplitude/rrweb@2.0.0-alpha.33", "", { "dependencies": { "@amplitude/rrdom": "^2.0.0-alpha.33", "@amplitude/rrweb-snapshot": "^2.0.0-alpha.33", "@amplitude/rrweb-types": "^2.0.0-alpha.33", "@amplitude/rrweb-utils": "^2.0.0-alpha.33", "@types/css-font-loading-module": "0.0.7", "@xstate/fsm": "^1.4.0", "base64-arraybuffer": "^1.0.1", "mitt": "^3.0.0" } }, "sha512-vMuk/3HzDWaUzBLFxKd7IpA8TEWjyPZBuLiLexMd/mOfTt/+JkVLsfXiJOyltJfR98LpmMTp1q51dtq357Dnfg=="], + + "@amplitude/rrweb-packer": ["@amplitude/rrweb-packer@2.0.0-alpha.32", "", { "dependencies": { "@amplitude/rrweb-types": "^2.0.0-alpha.32", "fflate": "^0.4.4" } }, "sha512-vYT0JFzle/FV9jIpEbuumCLh516az6ltAo7mrd06dlGo1tgos7bJbl3kcnvEXmDG7WWsKwip/Qprap7cZ4CmJw=="], + + "@amplitude/rrweb-plugin-console-record": ["@amplitude/rrweb-plugin-console-record@2.0.0-alpha.32", "", { "peerDependencies": { "@amplitude/rrweb": "^2.0.0-alpha.32" } }, "sha512-oJuBSNuBnqnrRCneW3b/pMirSz0Ubr2Ebz/t+zJhkGBgrTPNMviv8sSyyGuSn0kL4RAh/9QAG1H1hiYf9cuzgA=="], + + "@amplitude/rrweb-record": ["@amplitude/rrweb-record@2.0.0-alpha.32", "", { "dependencies": { "@amplitude/rrweb": "^2.0.0-alpha.32", "@amplitude/rrweb-types": "^2.0.0-alpha.32" } }, "sha512-bs5ItsPfedVNiZyIzYgtey6S6qaU90XcP4/313dcvedzBk9o+eVjBG5DDbStJnwYnSj+lB+oAWw5uc9H9ghKjQ=="], + + "@amplitude/rrweb-snapshot": ["@amplitude/rrweb-snapshot@2.0.0-alpha.33", "", { "dependencies": { "postcss": "^8.4.38" } }, "sha512-06CgbRFS+cYDo1tUa+Fe8eo4QA9qmYv9Azio3UYlYxqJf4BtAYSL0eXuzVBuqt3ZXnQwzBlsUj/8QWKKySkO7A=="], + + "@amplitude/rrweb-types": ["@amplitude/rrweb-types@2.0.0-alpha.32", "", {}, "sha512-tDs8uizkG+UwE2GKjXh+gH8WhUz0C3y7WfTwrtWi1TnsVc00sXaKSUo5G2h4YF4PGK6dpnLgJBqTwrqCZ211AQ=="], + + "@amplitude/rrweb-utils": ["@amplitude/rrweb-utils@2.0.0-alpha.32", "", {}, "sha512-DCCQjuNACkIMkdY5/KBaEgL4znRHU694ClW3RIjqFXJ6j6pqGyjEhCqtlCes+XwdgwOQKnJGMNka3J9rmrSqHg=="], + + "@amplitude/session-replay-browser": ["@amplitude/session-replay-browser@1.28.21", "", { "dependencies": { "@amplitude/analytics-client-common": "^2.4.8", "@amplitude/analytics-core": "^2.28.0", "@amplitude/analytics-remote-config": "^0.6.3", "@amplitude/analytics-types": "^2.10.0", "@amplitude/rrweb-packer": "2.0.0-alpha.32", "@amplitude/rrweb-plugin-console-record": "2.0.0-alpha.32", "@amplitude/rrweb-record": "2.0.0-alpha.32", "@amplitude/rrweb-types": "2.0.0-alpha.32", "@amplitude/rrweb-utils": "2.0.0-alpha.32", "@amplitude/targeting": "0.2.0", "@rollup/plugin-replace": "^6.0.1", "idb": "8.0.0", "tslib": "^2.4.1" } }, "sha512-wtoDO/wgThmimmpLOAgrJ69KGwBHCIiqC+WLQNRTsN+8o9Q1fAwBxrt+Z+jCh0DdBzsy18sBpMyI/VO22rpgpA=="], + + "@amplitude/targeting": ["@amplitude/targeting@0.2.0", "", { "dependencies": { "@amplitude/analytics-client-common": ">=1 <3", "@amplitude/analytics-core": ">=1 <3", "@amplitude/analytics-types": ">=1 <3", "@amplitude/experiment-core": "0.7.2", "idb": "^8.0.0", "tslib": "^2.4.1" } }, "sha512-/50ywTrC4hfcfJVBbh5DFbqMPPfaIOivZeb5Gb+OGM03QrA+lsUqdvtnKLNuWtceD4H6QQ2KFzPJ5aAJLyzVDA=="], + + "@amplitude/ua-parser-js": ["@amplitude/ua-parser-js@0.7.33", "", {}, "sha512-wKEtVR4vXuPT9cVEIJkYWnlF++Gx3BdLatPBM+SZ1ztVIvnhdGBZR/mn9x/PzyrMcRlZmyi6L56I2J3doVBnjA=="], + + "@amplitude/unified": ["@amplitude/unified@1.0.0-beta.9", "", { "dependencies": { "@amplitude/analytics-browser": "^2.25.2", "@amplitude/engagement-browser": "^1.0.3", "@amplitude/plugin-experiment-browser": "^1.0.0-beta.0", "@amplitude/plugin-session-replay-browser": "^1.16.0" } }, "sha512-JS7Dq6GJ6amR0olsVu0ekypAUHpcect8pJNTKjWYtEwdUbfQP6giE41Bum8rLR85SOMBqbuMdDr3yO9oj2CKyw=="], + +>>>>>>> staging "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.32.1", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-U9JwTrDvdQ9iWuABVsMLj8nJVwAyQz6QXvgLsVhryhCEPkLsbcP/MXxm+jYcAwLoV8ESbaTTjnD4kuAFa+Hyjg=="], + "@autumn/scripts": ["@autumn/scripts@workspace:scripts"], + "@autumn/server": ["@autumn/server@workspace:server"], "@autumn/shared": ["@autumn/shared@workspace:shared"], @@ -309,9 +388,17 @@ "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], +<<<<<<< HEAD "@better-auth/core": ["@better-auth/core@1.3.27", "", { "dependencies": { "better-call": "1.0.19", "zod": "^4.1.5" } }, "sha512-3Sfdax6MQyronY+znx7bOsfQHI6m1SThvJWb0RDscFEAhfqLy95k1sl+/PgGyg0cwc2cUXoEiAOSqYdFYrg3vA=="], "@better-auth/stripe": ["@better-auth/stripe@1.3.27", "", { "dependencies": { "defu": "^6.1.4", "zod": "^4.1.5" }, "peerDependencies": { "@better-auth/core": "1.3.27", "better-auth": "1.3.27", "stripe": "^18" } }, "sha512-YXmWMvX07lCDHheRB65jl80sSHTClFeJqN2vU6v5tI+0R4tzYBeWxYjvkS6cks1JJ8IjPUKTew0gy2Px3AlOCA=="], +======= + "@better-auth/core": ["@better-auth/core@1.3.28", "", { "dependencies": { "zod": "^4.1.5" }, "peerDependencies": { "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "better-call": "1.0.19", "better-sqlite3": "^12.4.1", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1" } }, "sha512-iZOGKlXaNEIEj0Q3z7+REE94I89YUJ0sel/1pvm1qqdHkm59G+ToTysHtyTcLYby3+UtAeJRKyFAY0nwJH0H7A=="], + + "@better-auth/stripe": ["@better-auth/stripe@1.3.28", "", { "dependencies": { "defu": "^6.1.4", "zod": "^4.1.5" }, "peerDependencies": { "@better-auth/core": "1.3.28", "better-auth": "1.3.28", "stripe": "^18" } }, "sha512-FGvQnIcLoMzNrfvzIVmT80/bBYyUwIQwHrG5KXbmTi26r9YTE0pylDiYD7RHX6q7H3wOntUXUyGDJ4b19h1+ew=="], + + "@better-auth/telemetry": ["@better-auth/telemetry@1.3.28", "", { "dependencies": { "@better-auth/core": "1.3.28", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18" } }, "sha512-qZtV82IFuyQZc2c37VkiDgO/qfqPnJuWIyeC/iFK1AA5N8RSuC2+CVIH1sNDytPXUAthbYeOzcOCW2YEkgz1Ow=="], +>>>>>>> staging "@better-auth/utils": ["@better-auth/utils@0.3.0", "", {}, "sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw=="], @@ -337,6 +424,7 @@ "@browserbasehq/sdk": ["@browserbasehq/sdk@2.6.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-83iXP5D7xMm8Wyn66TUaUrgoByCmAJuoMoZQI3sGg3JAiMlTfnCIMqyVBoNSaItaPIkaCnrsj6LiusmXV2X9YA=="], +<<<<<<< HEAD "@clerk/backend": ["@clerk/backend@2.18.0", "", { "dependencies": { "@clerk/shared": "^3.27.4", "@clerk/types": "^4.93.0", "cookie": "1.0.2", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-Vt7yP0FbNDptWGAsNYrCK572k+0m/jFDwXaHor4164rekwUJXunQVo8DX8vqCOZn9LsKUZbs4odmG9uoRVePIQ=="], "@clerk/express": ["@clerk/express@1.7.38", "", { "dependencies": { "@clerk/backend": "^2.18.0", "@clerk/shared": "^3.27.4", "@clerk/types": "^4.93.0", "tslib": "2.8.1" }, "peerDependencies": { "express": "^4.17.0 || ^5.0.0" } }, "sha512-+5SAeggm0847Ylvh6W/TGCqy/+ddhpzkYBdNNoPgjK13PuKkDodg7t5Wl8VR+Egs62y2Y9YEoTEQ6NE3MBd/sw=="], @@ -344,6 +432,15 @@ "@clerk/shared": ["@clerk/shared@3.27.4", "", { "dependencies": { "@clerk/types": "^4.93.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" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-K+glWUasxr9CeMHfOcpU+wiRhGQfQm1i7TQcM6tZuKmUD+DltryuF7sPjR+pj2ABUGSXt3Chlv2Y1i+8zaLLRw=="], "@clerk/types": ["@clerk/types@4.93.0", "", { "dependencies": { "csstype": "3.1.3" } }, "sha512-xrYAAF0OHCvZm5kPjCexff44ImhIC85LG7Z8OU3WEWvsGyeSnEjZuMTp5jZnns6i+hLQbxn5/cgINzC+V7VSpw=="], +======= + "@clerk/backend": ["@clerk/backend@2.18.3", "", { "dependencies": { "@clerk/shared": "^3.28.2", "@clerk/types": "^4.95.0", "cookie": "1.0.2", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-fWMq/Tb2hgfUXLKJN8jr6pbpA5XLUwC4BjWz7lB5Y+YhXhBrO7GtfpZIS91L/aDhNb17X6IaE6XvS6tDJBCUUw=="], + + "@clerk/express": ["@clerk/express@1.7.41", "", { "dependencies": { "@clerk/backend": "^2.18.3", "@clerk/shared": "^3.28.2", "@clerk/types": "^4.95.0", "tslib": "2.8.1" }, "peerDependencies": { "express": "^4.17.0 || ^5.0.0" } }, "sha512-SYKXi/Prjkxx15QGOjHlvjfwO05vUg6fBaxVdg53/vcLJNyyfER+JM9qRxxcmEqN5vkkpdqURANF3eZ8dkf85w=="], + + "@clerk/shared": ["@clerk/shared@3.28.2", "", { "dependencies": { "@clerk/types": "^4.95.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" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-BfBCPaoPoLCiU0b0MhQUfCjs+bWRRLkdHw0vBffSjtsFLxp1b5IL5D8nKgDPIKIIv7DmCCmO15tr+GqG3CGpYQ=="], + + "@clerk/types": ["@clerk/types@4.95.0", "", { "dependencies": { "csstype": "3.1.3" } }, "sha512-K1kI3BjvufG1mZBZJ5Q8Yu9wV6AFpjjITml5vhvP95xibJWOi3eYvlRCTKXDNKBFGvQfrTJbwn67jSG2VdyLKw=="], +>>>>>>> staging "@clickhouse/client": ["@clickhouse/client@1.12.1", "", { "dependencies": { "@clickhouse/client-common": "1.12.1" } }, "sha512-7ORY85rphRazqHzImNXMrh4vsaPrpetFoTWpZYueCO2bbO6PXYDXp/GQ4DgxnGIqbWB/Di1Ai+Xuwq2o7DJ36A=="], @@ -369,6 +466,7 @@ "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], +<<<<<<< HEAD "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.10", "", { "os": "aix", "cpu": "ppc64" }, "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.25.10", "", { "os": "android", "cpu": "arm" }, "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w=="], @@ -420,22 +518,83 @@ "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.10", "", { "os": "win32", "cpu": "ia32" }, "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw=="], "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.10", "", { "os": "win32", "cpu": "x64" }, "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw=="], +======= + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.11", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.11", "", { "os": "android", "cpu": "arm" }, "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.11", "", { "os": "android", "cpu": "arm64" }, "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.11", "", { "os": "android", "cpu": "x64" }, "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.11", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.11", "", { "os": "linux", "cpu": "arm" }, "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.11", "", { "os": "linux", "cpu": "ia32" }, "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.11", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.11", "", { "os": "linux", "cpu": "none" }, "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.11", "", { "os": "linux", "cpu": "s390x" }, "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.11", "", { "os": "linux", "cpu": "x64" }, "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.11", "", { "os": "none", "cpu": "x64" }, "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.11", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.11", "", { "os": "openbsd", "cpu": "x64" }, "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.11", "", { "os": "none", "cpu": "arm64" }, "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.11", "", { "os": "sunos", "cpu": "x64" }, "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.11", "", { "os": "win32", "cpu": "ia32" }, "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.11", "", { "os": "win32", "cpu": "x64" }, "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA=="], +>>>>>>> staging "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], - "@eslint/config-array": ["@eslint/config-array@0.21.0", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ=="], + "@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="], +<<<<<<< HEAD "@eslint/config-helpers": ["@eslint/config-helpers@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog=="], +======= + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.1", "", { "dependencies": { "@eslint/core": "^0.16.0" } }, "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw=="], +>>>>>>> staging "@eslint/core": ["@eslint/core@0.16.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q=="], "@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="], +<<<<<<< HEAD "@eslint/js": ["@eslint/js@9.37.0", "", {}, "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg=="], +======= + "@eslint/js": ["@eslint/js@9.38.0", "", {}, "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A=="], +>>>>>>> staging - "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.0", "", { "dependencies": { "@eslint/core": "^0.16.0", "levn": "^0.4.1" } }, "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A=="], @@ -611,6 +770,7 @@ "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], +<<<<<<< HEAD "@next/env": ["@next/env@15.5.5", "", {}, "sha512-2Zhvss36s/yL+YSxD5ZL5dz5pI6ki1OLxYlh6O77VJ68sBnlUrl5YqhBgCy7FkdMsp9RBeGFwpuDCdpJOqdKeQ=="], "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lYExGHuFIHeOxf40mRLWoA84iY2sLELB23BV5FIDHhdJkN1LpRTPc1MDOawgTo5ifbM5dvAwnGuHyNm60G1+jw=="], @@ -628,6 +788,25 @@ "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-GDgdNPFFqiKjTrmfw01sMMRWhVN5wOCmFzPloxa7ksDfX6TZt62tAK986f0ZYqWpvDFqeBCLAzmgTURvtQBdgw=="], "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.5", "", { "os": "win32", "cpu": "x64" }, "sha512-5kE3oRJxc7M8RmcTANP8RGoJkaYlwIiDD92gSwCjJY0+j8w8Sl1lvxgQ3bxfHY2KkHFai9tpy/Qx1saWV8eaJQ=="], +======= + "@next/env": ["@next/env@15.5.6", "", {}, "sha512-3qBGRW+sCGzgbpc5TS1a0p7eNxnOarGVQhZxfvTdnV0gFI61lX7QNtQ4V1TSREctXzYn5NetbUsLvyqwLFJM6Q=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ES3nRz7N+L5Umz4KoGfZ4XX6gwHplwPhioVRc25+QNsDa7RtUF/z8wJcbuQ2Tffm5RZwuN2A063eapoJ1u4nPg=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-JIGcytAyk9LQp2/nuVZPAtj8uaJ/zZhsKOASTjxDug0SPU9LAM3wy6nPU735M1OqacR4U20LHVF5v5Wnl9ptTA=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qvz4SVKQ0P3/Im9zcS2RmfFL/UCQnsJKJwQSkissbngnB/12c6bZTCB0gHTexz1s6d/mD0+egPKXAIRFVS7hQg=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-FsbGVw3SJz1hZlvnWD+T6GFgV9/NYDeLTNQB2MXoPN5u9VA9OEDy6fJEfePfsUKAhJufFbZLgp0cPxMuV6SV0w=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-3QnHGFWlnvAgyxFxt2Ny8PTpXtQD7kVEeaFat5oPAHHI192WKYB+VIKZijtHLGdBBvc16tiAkPTDmQNOQ0dyrA=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-OsGX148sL+TqMK9YFaPFPoIaJKbFJJxFzkXZljIgA9hjMjdruKht6xDCEv1HLtlLNfkx3c5w2GLKhj7veBQizQ=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-ONOMrqWxdzXDJNh2n60H6gGyKed42Ieu6UTVPZteXpuKbLZTH4G4eBMsr5qWgOBA+s7F+uB4OJbZnrkEDnZ5Fg=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-pxK4VIjFRx1MY92UycLOOw7dTdvccWsNETQ0kDHkBlcFH1GrTLUjSiHU1ohrznnux6TqRHgv5oflhfIWZwVROQ=="], +>>>>>>> staging "@noble/ciphers": ["@noble/ciphers@2.0.1", "", {}, "sha512-xHK3XHPUW8DTAobU+G0XT+/w+JLM7/8k1UFdB5xg/zTFPnFCobhftzw8wl4Lw2aq/Rvir5pxfZV5fEazmeCJ2g=="], @@ -795,7 +974,7 @@ "@opentelemetry/sql-common": ["@opentelemetry/sql-common@0.41.2", "", { "dependencies": { "@opentelemetry/core": "^2.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0" } }, "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ=="], - "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.2.2", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA=="], + "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.0", "", { "dependencies": { "@noble/hashes": "^2.0.1", "error-causes": "^3.0.2" }, "bin": { "cuid2": "bin/cuid2.js" } }, "sha512-dnBUdZHawCgqpp8bJhzFDAdkzci00nCN47EiW6TxD9OVfP+gh4qVnstXRRnBKW3hm9vpa+P7cod6jiBJdf7V+g=="], "@peculiar/asn1-android": ["@peculiar/asn1-android@2.5.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.5.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-t8A83hgghWQkcneRsgGs2ebAlRe54ns88p7ouv8PW2tzF1nAW4yHcL4uZKrFpIU+uszIRzTkcCuie37gpkId0A=="], @@ -822,6 +1001,11 @@ "@peculiar/x509": ["@peculiar/x509@1.14.0", "", { "dependencies": { "@peculiar/asn1-cms": "^2.5.0", "@peculiar/asn1-csr": "^2.5.0", "@peculiar/asn1-ecc": "^2.5.0", "@peculiar/asn1-pkcs9": "^2.5.0", "@peculiar/asn1-rsa": "^2.5.0", "@peculiar/asn1-schema": "^2.5.0", "@peculiar/asn1-x509": "^2.5.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg=="], "@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="], +<<<<<<< HEAD +======= + + "@pinojs/redact": ["@pinojs/redact@0.4.0", "", {}, "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="], +>>>>>>> staging "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], @@ -977,6 +1161,7 @@ "@react-email/text": ["@react-email/text@0.1.5", "", { "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg=="], +<<<<<<< HEAD "@reduxjs/toolkit": ["@reduxjs/toolkit@2.9.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^10.0.3", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], @@ -1024,6 +1209,59 @@ "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ=="], "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.4", "", { "os": "win32", "cpu": "x64" }, "sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w=="], +======= + "@reduxjs/toolkit": ["@reduxjs/toolkit@2.9.1", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^10.0.3", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-sETJ3qO72y7L7WiR5K54UFLT3jRzAtqeBPVO15xC3bGA6kDqCH8m/v7BKCPH4czydXzz/1lPEGLvew7GjOO3Qw=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], + + "@rollup/plugin-replace": ["@rollup/plugin-replace@6.0.2", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-7QaYCf8bqF04dOy7w/eHmJeNExxTYwvKAmlSAH/EaWWUzbT0h5sbF6bktFoX/0F/0qwng5/dWFMyf3gzaM8DsQ=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.5", "", { "os": "android", "cpu": "arm" }, "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.5", "", { "os": "android", "cpu": "arm64" }, "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.5", "", { "os": "linux", "cpu": "none" }, "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.5", "", { "os": "linux", "cpu": "x64" }, "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.5", "", { "os": "none", "cpu": "arm64" }, "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.52.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg=="], +>>>>>>> staging "@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="], @@ -1057,6 +1295,7 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], +<<<<<<< HEAD "@supabase/auth-js": ["@supabase/auth-js@2.75.0", "", { "dependencies": { "@supabase/node-fetch": "2.6.15" } }, "sha512-J8TkeqCOMCV4KwGKVoxmEBuDdHRwoInML2vJilthOo7awVCro2SM+tOcpljORwuBQ1vHUtV62Leit+5wlxrNtw=="], "@supabase/functions-js": ["@supabase/functions-js@2.75.0", "", { "dependencies": { "@supabase/node-fetch": "2.6.15" } }, "sha512-18yk07Moj/xtQ28zkqswxDavXC3vbOwt1hDuYM3/7xPnwwpKnsmPyZ7bQ5th4uqiJzQ135t74La9tuaxBR6e7w=="], @@ -1072,6 +1311,23 @@ "@supabase/storage-js": ["@supabase/storage-js@2.75.0", "", { "dependencies": { "@supabase/node-fetch": "2.6.15" } }, "sha512-wpJMYdfFDckDiHQaTpK+Ib14N/O2o0AAWWhguKvmmMurB6Unx17GGmYp5rrrqCTf8S1qq4IfIxTXxS4hzrUySg=="], "@supabase/supabase-js": ["@supabase/supabase-js@2.75.0", "", { "dependencies": { "@supabase/auth-js": "2.75.0", "@supabase/functions-js": "2.75.0", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "2.75.0", "@supabase/realtime-js": "2.75.0", "@supabase/storage-js": "2.75.0" } }, "sha512-8UN/vATSgS2JFuJlMVr51L3eUDz+j1m7Ww63wlvHLKULzCDaVWYzvacCjBTLW/lX/vedI2LBI4Vg+01G9ufsJQ=="], +======= + "@supabase/auth-js": ["@supabase/auth-js@2.75.1", "", { "dependencies": { "@supabase/node-fetch": "2.6.15" } }, "sha512-zktlxtXstQuVys/egDpVsargD9hQtG20CMdtn+mMn7d2Ulkzy2tgUT5FUtpppvCJtd9CkhPHO/73rvi5W6Am5A=="], + + "@supabase/functions-js": ["@supabase/functions-js@2.75.1", "", { "dependencies": { "@supabase/node-fetch": "2.6.15" } }, "sha512-xO+01SUcwVmmo67J7Htxq8FmhkYLFdWkxfR/taxBOI36wACEUNQZmroXGPl4PkpYxBO7TaDsRHYGxUpv9zTKkg=="], + + "@supabase/node-fetch": ["@supabase/node-fetch@2.6.15", "", { "dependencies": { "whatwg-url": "^5.0.0" } }, "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ=="], + + "@supabase/postgrest-js": ["@supabase/postgrest-js@2.75.1", "", { "dependencies": { "@supabase/node-fetch": "2.6.15" } }, "sha512-FiYBD0MaKqGW8eo4Xqu7/100Xm3ddgh+3qHtqS18yQRoglJTFRQCJzY1xkrGS0JFHE2YnbjL6XCiOBXiG8DK4Q=="], + + "@supabase/realtime-js": ["@supabase/realtime-js@2.75.1", "", { "dependencies": { "@supabase/node-fetch": "2.6.15", "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "ws": "^8.18.2" } }, "sha512-lBIJ855bUsBFScHA/AY+lxIFkubduUvmwbagbP1hq0wDBNAsYdg3ql80w8YmtXCDjkCwlE96SZqcFn7BGKKJKQ=="], + + "@supabase/ssr": ["@supabase/ssr@0.5.2", "", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^0.7.0" }, "peerDependencies": { "@supabase/supabase-js": "^2.43.4" } }, "sha512-n3plRhr2Bs8Xun1o4S3k1CDv17iH5QY9YcoEvXX3bxV1/5XSasA0mNXYycFmADIdtdE6BG9MRjP5CGIs8qxC8A=="], + + "@supabase/storage-js": ["@supabase/storage-js@2.75.1", "", { "dependencies": { "@supabase/node-fetch": "2.6.15" } }, "sha512-WdGEhroflt5O398Yg3dpf1uKZZ6N3CGloY9iGsdT873uWbkQKoP0wG8mtx98dh0fhj6dAlzBqOAvnlV12cJfzA=="], + + "@supabase/supabase-js": ["@supabase/supabase-js@2.75.1", "", { "dependencies": { "@supabase/auth-js": "2.75.1", "@supabase/functions-js": "2.75.1", "@supabase/node-fetch": "2.6.15", "@supabase/postgrest-js": "2.75.1", "@supabase/realtime-js": "2.75.1", "@supabase/storage-js": "2.75.1" } }, "sha512-GEPVBvjQimcMd9z5K1eTKTixTRb6oVbudoLQ9JKqTUJnR6GQdBU4OifFZean1AnHfsQwtri1fop2OWwsMv019w=="], +>>>>>>> staging "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], @@ -1105,11 +1361,19 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.1.14", "", { "dependencies": { "@tailwindcss/node": "4.1.14", "@tailwindcss/oxide": "4.1.14", "tailwindcss": "4.1.14" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA=="], +<<<<<<< HEAD "@tanstack/query-core": ["@tanstack/query-core@5.90.3", "", {}, "sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA=="], "@tanstack/query-devtools": ["@tanstack/query-devtools@5.90.1", "", {}, "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ=="], "@tanstack/react-query": ["@tanstack/react-query@5.90.3", "", { "dependencies": { "@tanstack/query-core": "5.90.3" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-i/LRL6DtuhG6bjGzavIMIVuKKPWx2AnEBIsBfuMm3YoHne0a20nWmsatOCBcVSaT0/8/5YFjNkebHAPLVUSi0Q=="], +======= + "@tanstack/query-core": ["@tanstack/query-core@5.90.5", "", {}, "sha512-wLamYp7FaDq6ZnNehypKI5fNvxHPfTYylE0m/ZpuuzJfJqhR5Pxg9gvGBHZx4n7J+V5Rg5mZxHHTlv25Zt5u+w=="], + + "@tanstack/query-devtools": ["@tanstack/query-devtools@5.90.1", "", {}, "sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.90.5", "", { "dependencies": { "@tanstack/query-core": "5.90.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-pN+8UWpxZkEJ/Rnnj2v2Sxpx1WFlaa9L6a4UO89p6tTQbeo+m0MS8oYDjbggrR8QcTyjKoYWKS3xJQGr3ExT8Q=="], +>>>>>>> staging "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.90.2", "", { "dependencies": { "@tanstack/query-devtools": "5.90.1" }, "peerDependencies": { "@tanstack/react-query": "^5.90.2", "react": "^18 || ^19" } }, "sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ=="], @@ -1159,6 +1423,11 @@ "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], +<<<<<<< HEAD +======= + "@types/css-font-loading-module": ["@types/css-font-loading-module@0.0.7", "", {}, "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q=="], + +>>>>>>> staging "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], @@ -1195,6 +1464,8 @@ "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + "@types/inquirer": ["@types/inquirer@9.0.9", "", { "dependencies": { "@types/through": "*", "rxjs": "^7.2.0" } }, "sha512-/mWx5136gts2Z2e5izdoRCo46lPp5TMs9R15GTSsgg/XnZyxDWVqoVU3R9lWnccKpqwsJLvRoxbCjoJtZB7DSw=="], + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], @@ -1215,7 +1486,11 @@ "@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="], +<<<<<<< HEAD "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], +======= + "@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="], +>>>>>>> staging "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], @@ -1259,6 +1534,8 @@ "@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="], + "@types/through": ["@types/through@0.0.33", "", { "dependencies": { "@types/node": "*" } }, "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ=="], + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], @@ -1289,6 +1566,7 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA=="], +<<<<<<< HEAD "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251014.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251014.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251014.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251014.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251014.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251014.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251014.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251014.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-IqmX5CYCBqXbfL+HKlcQAMaDlfJ0Z8OhUxvADFV2TENnzSYI4CuhvKxwOB2wFSLXufVsgtAlf3Fjwn24KmMyPQ=="], "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251014.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7rQoLlerWnwnvrM56hP4rdEbo4xDE4zr7cch+EzgENq/tbXYereGq1fmnR83UNglb1Eyy53OvJZ3O2csYBa2vg=="], @@ -1304,6 +1582,23 @@ "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251014.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-P0D4UEXwzFZh3pHexe2Ky1tW/HjY/HxTBTIajz2ViDCNPw7uDSEsXSB4H9TTiFJw8gVdTUFbsoAQp1MteTeORA=="], "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251014.1", "", { "os": "win32", "cpu": "x64" }, "sha512-fi53g2ihH7tkQLlz8hZGAb2V+3aNZpcxrZ530CQ4xcWwAqssEj0EaZJX0VLEtIQBar1ttGVK9Pz/wJU9sYyVzg=="], +======= + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251019.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251019.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251019.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251019.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251019.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251019.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251019.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251019.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-ytCPJouuNmJyGjZwSFg/v0Ugkn/52drU5HymW1p0l6dU+iHuTIaZSKfHFWETJxQVwyyYqNxxvC0QMxTDfwPlGQ=="], + + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251019.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GecLPUXgaptUiBrpuLhKwsxsckJ/rBA1e9pY2HdFx+mIWze1FTUiXu0It6EcFbQ2IZCMke1WuZZz18Bo4lftwA=="], + + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251019.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/XTRfbZW+BKvxC0XwoRp21UXdQOAEUwTf/T1OMs797HLfl1EbBiCp2UK+boYFwDw/5WP18i5bYEHkzx34wUTaA=="], + + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251019.1", "", { "os": "linux", "cpu": "arm" }, "sha512-m0dBydey0T9ToLVbB1e4keK2hSLqJPOT5RMQH9plxibU89Ry4ueON6yGvJgO4La0LqVyk5RqLSkuHyLGFHmevA=="], + + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251019.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-wJR4FDSvOBqtNIZ3SxXk72LfMMPdx69VXpavSgoyZY9Xkf7Wr6uNpKTmwzX/fOjQBhpHo/6ctiSjB/t3uVKKSQ=="], + + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251019.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xS6qSkZEKw/kw95+K/1xe/3ivx+M8bu5rrySSi+lZmvk2Og19pTmvyW4ec9ojleoYGrU3E6oAcCI+mbcO+KVKg=="], + + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251019.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-vJvkjZEN6GRH+Y3atQO5t8WGGjqgnHzwU0ZP+4oqYJl7G6sNiqRw23lHedZxZF6Iav+lE6SPMAhVbz97LlvkbQ=="], + + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251019.1", "", { "os": "win32", "cpu": "x64" }, "sha512-GMYGYxRHIX/+hFn7SGj9LMh4CLm90ZByGH4BvgKvwJGEctkYtOB6wXJUvQMo5koel7pDY1Yy3uChiexuG11biQ=="], +>>>>>>> staging "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -1313,6 +1608,8 @@ "@wooorm/starry-night": ["@wooorm/starry-night@3.8.0", "", { "dependencies": { "@types/hast": "^3.0.0", "import-meta-resolve": "^4.0.0", "vscode-oniguruma": "^2.0.0", "vscode-textmate": "^9.0.0" } }, "sha512-BWRm0tCzWCmv1ucBh6frL2uFRvYFj/LWmJzr+rJsdF/JsJ1+bBkeiyExH1iWQS18IH22HFu7f4QgG99OU1nklA=="], + "@xstate/fsm": ["@xstate/fsm@1.6.5", "", {}, "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], @@ -1389,7 +1686,11 @@ "bare-events": ["bare-events@2.8.0", "", { "peerDependencies": { "bare-abort-controller": "*" }, "optionalPeers": ["bare-abort-controller"] }, "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA=="], +<<<<<<< HEAD "bare-fs": ["bare-fs@4.4.10", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-arqVF+xX/rJHwrONZaSPhlzleT2gXwVs9rsAe1p1mIVwWZI2A76/raio+KwwxfWMO8oV9Wo90EaUkS2QwVmy4w=="], +======= + "bare-fs": ["bare-fs@4.4.11", "", { "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", "bare-stream": "^2.6.4", "bare-url": "^2.2.2", "fast-fifo": "^1.3.2" }, "peerDependencies": { "bare-buffer": "*" }, "optionalPeers": ["bare-buffer"] }, "sha512-Bejmm9zRMvMTRoHS+2adgmXw1ANZnCNx+B5dgZpGwlP1E3x6Yuxea8RToddHUbWtVV0iUMWqsgZr8+jcgUI2SA=="], +>>>>>>> staging "bare-os": ["bare-os@3.6.2", "", {}, "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A=="], @@ -1401,10 +1702,13 @@ "base-convert-int-array": ["base-convert-int-array@1.0.1", "", {}, "sha512-NWqzaoXx8L/SS32R+WmKqnQkVXVYl2PwNJ68QV3RAlRRL1uV+yxJT66abXI1cAvqCXQTyXr7/9NN4Af90/zDVw=="], + "base64-arraybuffer": ["base64-arraybuffer@1.0.2", "", {}, "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], +<<<<<<< HEAD "baseline-browser-mapping": ["baseline-browser-mapping@2.8.16", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw=="], "basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="], @@ -1412,11 +1716,24 @@ "better-auth": ["better-auth@1.3.27", "", { "dependencies": { "@better-auth/core": "1.3.27", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-SwiGAJ7yU6dBhNg0NdV1h5M8T5sa7/AszZVc4vBfMDrLLmvUfbt9JoJ0uRUJUEdKRAAxTyl9yA+F3+GhtAD80w=="], "better-call": ["better-call@1.0.19", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw=="], +======= + "baseline-browser-mapping": ["baseline-browser-mapping@2.8.18", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w=="], + + "basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="], + + "better-auth": ["better-auth@1.3.28", "", { "dependencies": { "@better-auth/core": "1.3.28", "@better-auth/telemetry": "1.3.28", "@better-auth/utils": "0.3.0", "@better-fetch/fetch": "1.1.18", "@noble/ciphers": "^2.0.0", "@noble/hashes": "^2.0.0", "@simplewebauthn/browser": "^13.1.2", "@simplewebauthn/server": "^13.1.2", "better-call": "1.0.19", "defu": "^6.1.4", "jose": "^6.1.0", "kysely": "^0.28.5", "nanostores": "^1.0.1", "zod": "^4.1.5" } }, "sha512-fSaeRsTSkzCSSKREFsm7z7TsTMC8ghGrwCN+mumxCZiyc8Fh/UThUwURlTJmsR0YVB0DMR8ejQH+c38WhdQslQ=="], + + "better-call": ["better-call@1.0.19", "", { "dependencies": { "@better-auth/utils": "^0.3.0", "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw=="], + + "better-sqlite3": ["better-sqlite3@12.4.1", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ=="], +>>>>>>> staging "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "body-parser": ["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" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], @@ -1453,7 +1770,11 @@ "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], +<<<<<<< HEAD "caniuse-lite": ["caniuse-lite@1.0.30001750", "", {}, "sha512-cuom0g5sdX6rw00qOoLNSFCJ9/mYIsuSOA+yzpDw8eopiFqcVwQvZHqov0vmEighRxX++cfC0Vg1G+1Iy/mSpQ=="], +======= + "caniuse-lite": ["caniuse-lite@1.0.30001751", "", {}, "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw=="], +>>>>>>> staging "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -1533,7 +1854,11 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], +<<<<<<< HEAD "convex": ["convex@1.27.5", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" }, "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" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-6YU/AVPnoNdAaJABKBI9c5IqRSKsow/c4yo/ntaOWtd8Dff2P2zaImA/ougICfPgTuTvjKRbgkxk6lJhODzb4g=="], +======= + "convex": ["convex@1.28.0", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" }, "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" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-40FgeJ/LxP9TxnkDDztU/A5gcGTdq1klcTT5mM0Ak+kSlQiDktMpjNX1TfkWLxXaE3lI4qvawKH95v2RiYgFxA=="], +>>>>>>> staging "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], @@ -1611,8 +1936,15 @@ "decode-uri-component": ["decode-uri-component@0.4.1", "", {}, "sha512-+8VxcR21HhTy8nOt6jf20w0c9CADrw1O8d+VZ/YzzCt4bJ3uBjw+D1q2osAB8RnpwwaeYBxy0HyKQxD5JBMuuQ=="], +<<<<<<< HEAD +======= + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + +>>>>>>> staging "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], @@ -1679,7 +2011,11 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], +<<<<<<< HEAD "electron-to-chromium": ["electron-to-chromium@1.5.235", "", {}, "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ=="], +======= + "electron-to-chromium": ["electron-to-chromium@1.5.237", "", {}, "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg=="], +>>>>>>> staging "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], @@ -1695,6 +2031,8 @@ "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "error-causes": ["error-causes@3.0.2", "", {}, "sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw=="], + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -1705,7 +2043,11 @@ "es-toolkit": ["es-toolkit@1.40.0", "", {}, "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q=="], +<<<<<<< HEAD "esbuild": ["esbuild@0.25.10", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.10", "@esbuild/android-arm": "0.25.10", "@esbuild/android-arm64": "0.25.10", "@esbuild/android-x64": "0.25.10", "@esbuild/darwin-arm64": "0.25.10", "@esbuild/darwin-x64": "0.25.10", "@esbuild/freebsd-arm64": "0.25.10", "@esbuild/freebsd-x64": "0.25.10", "@esbuild/linux-arm": "0.25.10", "@esbuild/linux-arm64": "0.25.10", "@esbuild/linux-ia32": "0.25.10", "@esbuild/linux-loong64": "0.25.10", "@esbuild/linux-mips64el": "0.25.10", "@esbuild/linux-ppc64": "0.25.10", "@esbuild/linux-riscv64": "0.25.10", "@esbuild/linux-s390x": "0.25.10", "@esbuild/linux-x64": "0.25.10", "@esbuild/netbsd-arm64": "0.25.10", "@esbuild/netbsd-x64": "0.25.10", "@esbuild/openbsd-arm64": "0.25.10", "@esbuild/openbsd-x64": "0.25.10", "@esbuild/openharmony-arm64": "0.25.10", "@esbuild/sunos-x64": "0.25.10", "@esbuild/win32-arm64": "0.25.10", "@esbuild/win32-ia32": "0.25.10", "@esbuild/win32-x64": "0.25.10" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ=="], +======= + "esbuild": ["esbuild@0.25.11", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.11", "@esbuild/android-arm": "0.25.11", "@esbuild/android-arm64": "0.25.11", "@esbuild/android-x64": "0.25.11", "@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-x64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-x64": "0.25.11", "@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-x64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-x64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-x64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.11", "@esbuild/sunos-x64": "0.25.11", "@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-x64": "0.25.11" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q=="], +>>>>>>> staging "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], @@ -1717,11 +2059,19 @@ "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], +<<<<<<< HEAD "eslint": ["eslint@9.37.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", "@eslint/config-helpers": "^0.4.0", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.37.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "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.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig=="], "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.23", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA=="], +======= + "eslint": ["eslint@9.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.1", "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.38.0", "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "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.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], + + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.24", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w=="], +>>>>>>> staging "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], @@ -1737,6 +2087,8 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], @@ -1749,6 +2101,11 @@ "events-universal": ["events-universal@1.0.1", "", { "dependencies": { "bare-events": "^2.7.0" } }, "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw=="], +<<<<<<< HEAD +======= + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + +>>>>>>> staging "express": ["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.4", "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" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], "express-rate-limit": ["express-rate-limit@7.5.1", "", { "peerDependencies": { "express": ">= 4.11" } }, "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw=="], @@ -1791,6 +2148,8 @@ "file-selector": ["file-selector@2.1.2", "", { "dependencies": { "tslib": "^2.7.0" } }, "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "filter-obj": ["filter-obj@5.1.0", "", {}, "sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng=="], @@ -1825,6 +2184,8 @@ "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1853,6 +2214,8 @@ "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -1891,7 +2254,11 @@ "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], +<<<<<<< HEAD "hono": ["hono@4.9.12", "", {}, "sha512-SrTC0YxqPwnN7yKa8gg/giLyQ2pILCKoideIHbYbFQlWZjYt68D2A4Ae1hehO/aDQ6RmTcpqOV/O2yBtMzx/VQ=="], +======= + "hono": ["hono@4.10.1", "", {}, "sha512-rpGNOfacO4WEPClfkEt1yfl8cbu10uB1lNpiI33AKoiAHwOS8lV748JiLx4b5ozO/u4qLjIvfpFsPXdY5Qjkmg=="], +>>>>>>> staging "html-minifier-terser": ["html-minifier-terser@6.1.0", "", { "dependencies": { "camel-case": "^4.1.2", "clean-css": "^5.2.2", "commander": "^8.3.0", "he": "^1.2.0", "param-case": "^3.0.4", "relateurl": "^0.2.7", "terser": "^5.10.0" }, "bin": { "html-minifier-terser": "cli.js" } }, "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw=="], @@ -1917,6 +2284,10 @@ "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "idb": ["idb@8.0.0", "", {}, "sha512-l//qvlAKGmQO31Qn7xdzagVPPaHTxXx199MhrAFuVBTPqydcPYBWjkrbv4Y0ktB+GmWOiwHl237UUOrLmQxLvw=="], + + "idb-keyval": ["idb-keyval@6.2.2", "", {}, "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], @@ -1935,6 +2306,8 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "input-otp": ["input-otp@1.4.2", "", { "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" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], "inquirer": ["inquirer@12.10.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/core": "^10.3.0", "@inquirer/prompts": "^7.9.0", "@inquirer/type": "^3.0.9", "mute-stream": "^2.0.0", "run-async": "^4.0.5", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-K/epfEnDBZj2Q3NMDcgXWZye3nhSPeoJnOh8lcKWrldw54UEZfS4EmAMsAsmVbl7qKi+vjAsy39Sz4fbgRMewg=="], @@ -1991,6 +2364,8 @@ "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + "js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="], "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], @@ -2159,6 +2534,8 @@ "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], @@ -2169,6 +2546,11 @@ "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], +<<<<<<< HEAD +======= + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + +>>>>>>> staging "mocha": ["mocha@11.7.4", "", { "dependencies": { "browser-stdout": "^1.3.1", "chokidar": "^4.0.1", "debug": "^4.3.5", "diff": "^7.0.0", "escape-string-regexp": "^4.0.0", "find-up": "^5.0.0", "glob": "^10.4.5", "he": "^1.2.0", "is-path-inside": "^3.0.3", "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.2.0", "yargs": "^17.7.2", "yargs-parser": "^21.1.1", "yargs-unparser": "^2.0.0" }, "bin": { "mocha": "bin/mocha.js", "_mocha": "bin/_mocha" } }, "sha512-1jYAaY8x0kAZ0XszLWu14pzsf4KV740Gld4HXkhNTXwcHx4AUEDkPzgEHg9CM5dVcW+zv036tjpsEbLraPJj4w=="], "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], @@ -2192,6 +2574,11 @@ "nanoid": ["nanoid@5.1.6", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg=="], "nanostores": ["nanostores@1.0.1", "", {}, "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw=="], +<<<<<<< HEAD +======= + + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], +>>>>>>> staging "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], @@ -2199,12 +2586,21 @@ "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], +<<<<<<< HEAD "next": ["next@15.5.5", "", { "dependencies": { "@next/env": "15.5.5", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.5", "@next/swc-darwin-x64": "15.5.5", "@next/swc-linux-arm64-gnu": "15.5.5", "@next/swc-linux-arm64-musl": "15.5.5", "@next/swc-linux-x64-gnu": "15.5.5", "@next/swc-linux-x64-musl": "15.5.5", "@next/swc-win32-arm64-msvc": "15.5.5", "@next/swc-win32-x64-msvc": "15.5.5", "sharp": "^0.34.3" }, "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" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-OQVdBPtpBfq7HxFN0kOVb7rXXOSIkt5lTzDJDGRBcOyVvNRIWFauMqi1gIHd1pszq1542vMOGY0HP4CaiALfkA=="], +======= + "next": ["next@15.5.6", "", { "dependencies": { "@next/env": "15.5.6", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.6", "@next/swc-darwin-x64": "15.5.6", "@next/swc-linux-arm64-gnu": "15.5.6", "@next/swc-linux-arm64-musl": "15.5.6", "@next/swc-linux-x64-gnu": "15.5.6", "@next/swc-linux-x64-musl": "15.5.6", "@next/swc-win32-arm64-msvc": "15.5.6", "@next/swc-win32-x64-msvc": "15.5.6", "sharp": "^0.34.3" }, "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" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ=="], +>>>>>>> staging "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], +<<<<<<< HEAD +======= + "node-abi": ["node-abi@3.78.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ=="], + +>>>>>>> staging "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], @@ -2213,7 +2609,11 @@ "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], +<<<<<<< HEAD "node-releases": ["node-releases@2.0.23", "", {}, "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg=="], +======= + "node-releases": ["node-releases@2.0.25", "", {}, "sha512-4auku8B/vw5psvTiiN9j1dAOsXvMoGqJuKJcR+dTdqiXEK20mMTk1UEo3HS16LeGQsVG6+qKTPM9u/qQ2LqATA=="], +>>>>>>> staging "nodemon": ["nodemon@3.1.10", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw=="], @@ -2221,7 +2621,11 @@ "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], +<<<<<<< HEAD "nuqs": ["nuqs@2.7.1", "", { "dependencies": { "@standard-schema/spec": "1.0.0" }, "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" }, "optionalPeers": ["@remix-run/react", "@tanstack/react-router", "next", "react-router", "react-router-dom"] }, "sha512-3WDgrOZWat0QyOheyljTlXK4TGFh1JKSLvXMgusMDcTyMJXe1xL8+q3zuQ6ke1vyeGnpJwztlZl2aDkMW2eIUg=="], +======= + "nuqs": ["nuqs@2.7.2", "", { "dependencies": { "@standard-schema/spec": "1.0.0" }, "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" }, "optionalPeers": ["@remix-run/react", "@tanstack/react-router", "next", "react-router", "react-router-dom"] }, "sha512-wOPJoz5om7jMJQick9zU1S/Q+joL+B2DZTZxfCleHEcUzjUnPoujGod4+nAmUWb+G9TwZnyv+mfNqlyfEi8Zag=="], +>>>>>>> staging "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -2301,7 +2705,11 @@ "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], +<<<<<<< HEAD "pino": ["pino@9.13.1", "", { "dependencies": { "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "slow-redact": "^0.3.0", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-Szuj+ViDTjKPQYiKumGmEn3frdl+ZPSdosHyt9SnUevFosOkMY2b7ipxlEctNKPmMD/VibeBI+ZcZCJK+4DPuw=="], +======= + "pino": ["pino@9.14.0", "", { "dependencies": { "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w=="], +>>>>>>> staging "pino-abstract-transport": ["pino-abstract-transport@1.2.0", "", { "dependencies": { "readable-stream": "^4.0.0", "split2": "^4.0.0" } }, "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q=="], @@ -2323,11 +2731,20 @@ "postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="], +<<<<<<< HEAD "posthog-js": ["posthog-js@1.275.3", "", { "dependencies": { "@posthog/core": "1.3.0", "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" }, "peerDependencies": { "@rrweb/types": "2.0.0-alpha.17", "rrweb-snapshot": "2.0.0-alpha.17" }, "optionalPeers": ["@rrweb/types", "rrweb-snapshot"] }, "sha512-LitwVprl0Q8p0fN4O4ThvlOuO6r+TBzLfkGbSyI5tR/YhlWzX3yFf4KKHPXJgxge99sxKa0fuVKNzcspYsDzcg=="], +======= + "posthog-js": ["posthog-js@1.276.0", "", { "dependencies": { "@posthog/core": "1.3.0", "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" }, "peerDependencies": { "@rrweb/types": "2.0.0-alpha.17", "rrweb-snapshot": "2.0.0-alpha.17" }, "optionalPeers": ["@rrweb/types", "rrweb-snapshot"] }, "sha512-FYZE1037LrAoKKeUU0pUL7u8WwNK2BVeg5TFApwquVPUdj9h7u5Z077A313hPN19Ar+7Y+VHxqYqdHc4VNsVgw=="], +>>>>>>> staging "posthog-node": ["posthog-node@4.18.0", "", { "dependencies": { "axios": "^1.8.2" } }, "sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw=="], "preact": ["preact@10.27.2", "", {}, "sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg=="], +<<<<<<< HEAD +======= + + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], +>>>>>>> staging "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], @@ -2363,7 +2780,11 @@ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], +<<<<<<< HEAD "puppeteer-core": ["puppeteer-core@24.24.1", "", { "dependencies": { "@puppeteer/browsers": "2.10.12", "chromium-bidi": "9.1.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1508733", "typed-query-selector": "^2.12.0", "webdriver-bidi-protocol": "0.3.7", "ws": "^8.18.3" } }, "sha512-4R9/hCjmyUBbQqjrCa+y4Pzgl3LneLfqB+Whh2JujA5Wzg+prnO60GxDPjAJmM+uirYxDx/8jIm0hGu8yDTyiA=="], +======= + "puppeteer-core": ["puppeteer-core@24.25.0", "", { "dependencies": { "@puppeteer/browsers": "2.10.12", "chromium-bidi": "9.1.0", "debug": "^4.4.3", "devtools-protocol": "0.0.1508733", "typed-query-selector": "^2.12.0", "webdriver-bidi-protocol": "0.3.7", "ws": "^8.18.3" } }, "sha512-8Xs6q3Ut+C8y7sAaqjIhzv1QykGWG4gc2mEZ2mYE7siZFuRp4xQVehOf8uQKSQAkeL7jXUs3mknEeiqnRqUKvQ=="], +>>>>>>> staging "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], @@ -2385,6 +2806,8 @@ "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], "react-cmdk": ["react-cmdk@1.3.9", "", { "dependencies": { "@headlessui/react": "^1.6.4", "@heroicons/react": "^2.0.13", "html-webpack-plugin": "^5.5.0" }, "peerDependencies": { "react": "^16.x || ^17.x || ^18.x", "react-dom": "^16.x || ^17.x || ^18.x" } }, "sha512-MSVmAQZ9iqY7hO3r++XP6yWSHzGfMDGMvY3qlDT8k5RiWoRFwO1CGPlsWzhvcUbPilErzsMKK7uB4McEcX4B6g=="], @@ -2433,7 +2856,11 @@ "recaseai": ["recaseai@0.0.37", "", { "dependencies": { "@anthropic-ai/sdk": "^0.32.1", "@supabase/supabase-js": "^2.47.2", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/node": "^22.10.1", "async-listen": "^3.0.1", "axios": "^1.7.9", "commander": "^12.1.0", "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", "figures": "^6.1.0", "inquirer": "^12.1.0", "ksuid": "^3.0.0", "nanoid": "^5.0.9", "openai": "^4.76.0", "ora": "^8.1.1", "picocolors": "^1.1.1", "pino": "^9.5.0", "tsx": "^4.19.2", "typescript": "^5.7.2" }, "bin": { "recase": "dist/cli.js" } }, "sha512-cKVMWTGBnGtm8K+uD2vfMXOzxdHj1U3++vvTePpOoZABI/SY/jjDpW997vM5xeDnzz7Vk/qqVkLQUQ/ZMDXpsQ=="], +<<<<<<< HEAD "recharts": ["recharts@3.2.1", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "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" } }, "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw=="], +======= + "recharts": ["recharts@3.3.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "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" } }, "sha512-Vi0qmTB0iz1+/Cz9o5B7irVyUjX2ynvEgImbgMt/3sKRREcUM07QiYjS1QpAVrkmVlXqy5gykq4nGWMz9AS4Rg=="], +>>>>>>> staging "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], @@ -2467,7 +2894,11 @@ "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], +<<<<<<< HEAD "rollup": ["rollup@4.52.4", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.4", "@rollup/rollup-android-arm64": "4.52.4", "@rollup/rollup-darwin-arm64": "4.52.4", "@rollup/rollup-darwin-x64": "4.52.4", "@rollup/rollup-freebsd-arm64": "4.52.4", "@rollup/rollup-freebsd-x64": "4.52.4", "@rollup/rollup-linux-arm-gnueabihf": "4.52.4", "@rollup/rollup-linux-arm-musleabihf": "4.52.4", "@rollup/rollup-linux-arm64-gnu": "4.52.4", "@rollup/rollup-linux-arm64-musl": "4.52.4", "@rollup/rollup-linux-loong64-gnu": "4.52.4", "@rollup/rollup-linux-ppc64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-gnu": "4.52.4", "@rollup/rollup-linux-riscv64-musl": "4.52.4", "@rollup/rollup-linux-s390x-gnu": "4.52.4", "@rollup/rollup-linux-x64-gnu": "4.52.4", "@rollup/rollup-linux-x64-musl": "4.52.4", "@rollup/rollup-openharmony-arm64": "4.52.4", "@rollup/rollup-win32-arm64-msvc": "4.52.4", "@rollup/rollup-win32-ia32-msvc": "4.52.4", "@rollup/rollup-win32-x64-gnu": "4.52.4", "@rollup/rollup-win32-x64-msvc": "4.52.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ=="], +======= + "rollup": ["rollup@4.52.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.52.5", "@rollup/rollup-android-arm64": "4.52.5", "@rollup/rollup-darwin-arm64": "4.52.5", "@rollup/rollup-darwin-x64": "4.52.5", "@rollup/rollup-freebsd-arm64": "4.52.5", "@rollup/rollup-freebsd-x64": "4.52.5", "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", "@rollup/rollup-linux-arm-musleabihf": "4.52.5", "@rollup/rollup-linux-arm64-gnu": "4.52.5", "@rollup/rollup-linux-arm64-musl": "4.52.5", "@rollup/rollup-linux-loong64-gnu": "4.52.5", "@rollup/rollup-linux-ppc64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-gnu": "4.52.5", "@rollup/rollup-linux-riscv64-musl": "4.52.5", "@rollup/rollup-linux-s390x-gnu": "4.52.5", "@rollup/rollup-linux-x64-gnu": "4.52.5", "@rollup/rollup-linux-x64-musl": "4.52.5", "@rollup/rollup-openharmony-arm64": "4.52.5", "@rollup/rollup-win32-arm64-msvc": "4.52.5", "@rollup/rollup-win32-ia32-msvc": "4.52.5", "@rollup/rollup-win32-x64-gnu": "4.52.5", "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw=="], +>>>>>>> staging "rou3": ["rou3@0.6.3", "", {}, "sha512-1HSG1ENTj7Kkm5muMnXuzzfdDOf7CFnbSYFA+H3Fp/rB9lOCxCPgy1jlZxTKyFoC5jJay8Mmc+VbPLYRjzYLrA=="], @@ -2525,6 +2956,13 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], +<<<<<<< HEAD +======= + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + +>>>>>>> staging "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], @@ -2589,7 +3027,11 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], +<<<<<<< HEAD "stripe": ["stripe@18.5.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA=="], +======= + "stripe": ["stripe@18.4.0-beta.2", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-4MCaxGkwZcCpMpgiE+Wb4hWwKTlnZgUf4pTlvIolxdrYAA5gb6MnIEJJAowrcrSnl41FvjQP0xV4QvdN2Fq8Zw=="], +>>>>>>> staging "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], @@ -2670,6 +3112,11 @@ "tsx": ["tsx@4.20.6", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg=="], "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], +<<<<<<< HEAD +======= + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], +>>>>>>> staging "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -2689,7 +3136,9 @@ "undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="], - "unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="], + "unfetch": ["unfetch@4.1.0", "", {}, "sha512-crP/n3eAPUJxZXM9T80/yv0YhkTEx2K1D3h7D1AJM6fzsWZrxdyRuLN0JH/dkZh1LNH8LxCnBzoPFCPbb2iGpg=="], + + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], @@ -2697,7 +3146,7 @@ "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], - "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], + "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], @@ -2731,7 +3180,11 @@ "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], +<<<<<<< HEAD "vite": ["vite@6.3.7", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-mQYaKepA0NGMBsz8Xktt3tJUG5ELE2iT7IJ+ssXI6nxVdE2sFc/d/6w/JByqMLvWg8hNKHpPgzjgOkrhpKFnrA=="], +======= + "vite": ["vite@6.4.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-oLnWs9Hak/LOlKjeSpOwD6JMks8BeICEdYMJBf6P4Lac/pO9tKiv/XhXnAM7nNfSkZahjlCZu9sS50zL8fSnsw=="], +>>>>>>> staging "vscode-oniguruma": ["vscode-oniguruma@2.0.1", "", {}, "sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ=="], @@ -2803,14 +3256,44 @@ "@ai-sdk/provider-utils/secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], +<<<<<<< HEAD "@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], "@autumn/server/@types/node": ["@types/node@22.18.10", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg=="], "@autumn/vite/@types/node": ["@types/node@22.18.10", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg=="], +======= + "@amplitude/analytics-client-common/@amplitude/analytics-types": ["@amplitude/analytics-types@2.10.0", "", {}, "sha512-WP8eEbJh10MmFVnxkHjg92i5DBxBFsRvSZxjDQPXEGL8ZP+i7rSsleiH2K3VrwoKksYfZ/1eAqrZvevAmjSlig=="], + + "@amplitude/analytics-remote-config/@amplitude/analytics-core": ["@amplitude/analytics-core@1.2.8", "", { "dependencies": { "@amplitude/analytics-types": "^1.4.0", "tslib": "^2.4.1" } }, "sha512-Krxpr5uvS3HmmjvpYqPfbMbs2kcZZu09L+6KwQnPiofWRzoXWIM217fRfy6aSD/QrAoPGbZjvtVitw9cB7Cx+A=="], + + "@amplitude/plugin-session-replay-browser/@amplitude/analytics-types": ["@amplitude/analytics-types@2.10.0", "", {}, "sha512-WP8eEbJh10MmFVnxkHjg92i5DBxBFsRvSZxjDQPXEGL8ZP+i7rSsleiH2K3VrwoKksYfZ/1eAqrZvevAmjSlig=="], + + "@amplitude/plugin-web-vitals-browser/web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="], + + "@amplitude/rrweb/@amplitude/rrweb-types": ["@amplitude/rrweb-types@2.0.0-alpha.33", "", {}, "sha512-OTUqndbcuXDZczf99NUq2PqQWTZ4JHK7oF8YT7aOXh1pJVEWhfe6S+J0idHd3YFCy1TD9gtOcdnz5nDJN68Wnw=="], + + "@amplitude/rrweb/@amplitude/rrweb-utils": ["@amplitude/rrweb-utils@2.0.0-alpha.33", "", {}, "sha512-brK6csN0Tj1W5gYERFhamWEPeFLbz9nYokdaUtd8PL/Y0owWXNX11KGP4pMWvl/f1bElDU0vcu3uYAzM4YGLQw=="], + + "@amplitude/session-replay-browser/@amplitude/analytics-types": ["@amplitude/analytics-types@2.10.0", "", {}, "sha512-WP8eEbJh10MmFVnxkHjg92i5DBxBFsRvSZxjDQPXEGL8ZP+i7rSsleiH2K3VrwoKksYfZ/1eAqrZvevAmjSlig=="], + + "@amplitude/targeting/@amplitude/analytics-types": ["@amplitude/analytics-types@2.10.0", "", {}, "sha512-WP8eEbJh10MmFVnxkHjg92i5DBxBFsRvSZxjDQPXEGL8ZP+i7rSsleiH2K3VrwoKksYfZ/1eAqrZvevAmjSlig=="], + + "@amplitude/targeting/@amplitude/experiment-core": ["@amplitude/experiment-core@0.7.2", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-Wc2NWvgQ+bLJLeF0A9wBSPIaw0XuqqgkPKsoNFQrmS7r5Djd56um75In05tqmVntPJZRvGKU46pAp8o5tdf4mA=="], + + "@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "@autumn/server/@types/node": ["@types/node@22.18.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ=="], + + "@autumn/shared/stripe": ["stripe@18.5.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA=="], + + "@autumn/vite/@types/node": ["@types/node@22.18.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ=="], +>>>>>>> staging "@autumn/vite/date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], + "@autumn/vite/stripe": ["stripe@18.5.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA=="], + "@autumn/vite/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], "@autumn/vite/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], @@ -2825,6 +3308,11 @@ "@better-auth/core/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], +<<<<<<< HEAD +======= + "@better-auth/stripe/stripe": ["stripe@18.5.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-Hp+wFiEQtCB0LlNgcFh5uVyKznpDjzyUZ+CNVEf+I3fhlYvh7rZruIg+jOwzJRCpy0ZTPMjlzm7J2/M2N6d+DA=="], + +>>>>>>> staging "@better-auth/stripe/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="], "@browserbasehq/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], @@ -3136,8 +3624,11 @@ "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.1.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ=="], "@opentelemetry/sql-common/@opentelemetry/core": ["@opentelemetry/core@2.1.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ=="], +<<<<<<< HEAD "@paralleldrive/cuid2/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], +======= +>>>>>>> staging "@sentry/node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@1.30.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA=="], @@ -3203,8 +3694,11 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], +<<<<<<< HEAD "@types/serve-static/@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], +======= +>>>>>>> staging "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -3293,8 +3787,12 @@ "postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "prebuild-install/tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], + "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "react-day-picker/date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], "react-email/glob": ["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.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA=="], @@ -3305,7 +3803,11 @@ "react-router/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], +<<<<<<< HEAD "recaseai/@types/node": ["@types/node@22.18.10", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg=="], +======= + "recaseai/@types/node": ["@types/node@22.18.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ=="], +>>>>>>> staging "recaseai/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], @@ -3744,6 +4246,13 @@ "ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], "ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], +<<<<<<< HEAD +======= + + "prebuild-install/tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "prebuild-install/tar-fs/tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], +>>>>>>> staging "react-email/glob/jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], @@ -3851,6 +4360,11 @@ "nodemon/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], +<<<<<<< HEAD +======= + "prebuild-install/tar-fs/tar-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + +>>>>>>> staging "react-email/glob/path-scurry/lru-cache": ["lru-cache@11.2.2", "", {}, "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg=="], "renderkid/htmlparser2/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], diff --git a/package.json b/package.json index ba6a5e5fb..788570b42 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "workspaces": [ "server", "shared", - "vite" + "vite", + "scripts" ], "type": "module", "scripts": { @@ -12,6 +13,7 @@ "dev:simple": "concurrently \"cd shared && bun dev:watch\" \"cd server && bun dev\" \"cd server && bun workers:dev\" \"cd vite && bun dev\"", "vite:build": "bun -F @autumn/shared build && bun -F @autumn/vite build:bun", "vite:start": "bun -F @autumn/vite start:bun", + "shared": "bun -F @autumn/shared build", "server": "bun -F @autumn/shared build && bun -F @autumn/server start", "workers": "bun -F @autumn/shared build && bun -F @autumn/server workers", @@ -20,14 +22,19 @@ "server:cron": "pnpm -F server cron:start", "server:check": "NODE_ENV=production pnpm -F server check", "setup": "node scripts/setup.js", + + "setup:test": "bun scripts/setup-test.ts", + "tests": "bun scripts/test.ts", + "setupci": "node scripts/setupci.js", "db:push": " bun -F @autumn/shared db:push", "db:generate": "bun -F @autumn/shared db:generate", "db:migrate": " bun -F @autumn/shared db:migrate", + "docker:up": "docker compose -f docker-compose.dev.yml up --build", "docker:up:unix": "docker compose -f docker-compose.unix.yml up --build", "docker:up:ci": "docker compose -f docker-compose.ci.yml up --build", - "build:all": "pnpm -F shared build && pnpm -F server prod:build && pnpm -F vite build", + "vite:build:bun": "bun -F @autumn/shared build && bun -F @autumn/vite build:bun", "vite:start:bun": "bun -F @autumn/shared build && bun -F @autumn/vite start:bun" }, diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 000000000..2da9851be --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,21 @@ +{ + "name": "@autumn/scripts", + "version": "1.0.0", + "type": "module", + "private": true, + "scripts": { + "setup": "tsx setup.js", + "setup-test": "tsx setup-test.ts" + }, + "dependencies": { + "@autumn/shared": "workspace:*", + "chalk": "^5.3.0", + "dotenv": "^16.5.0", + "inquirer": "^12.6.3" + }, + "devDependencies": { + "@types/inquirer": "^9.0.7", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } +} diff --git a/scripts/setup-test.ts b/scripts/setup-test.ts new file mode 100644 index 000000000..d4ddec866 --- /dev/null +++ b/scripts/setup-test.ts @@ -0,0 +1,145 @@ +#!/usr/bin/env node +import chalk from "chalk"; +import inquirer from "inquirer"; +import { createTestOrg, TEST_ORG_CONFIG } from "./setupTestUtils/createTestOrg.js"; +import { + setupStripeTestKey, + setupTunnelUrl, + setupUpstash, +} from "./setupTestUtils/setupPrompts.js"; +import { updateEnvFile } from "./setupTestUtils/updateEnvFile.js"; +import { + updateSingleEnvVar, + updateMultipleEnvVars, +} from "./setupTestUtils/incrementalEnvUpdate.js"; + +async function showPreparationChecklist() { + console.log( + chalk.magentaBright("\n================ Autumn Test Setup ================\n"), + ); + console.log( + chalk.cyan("This script will set up a test organization for development.\n"), + ); + console.log(chalk.yellowBright("Before you begin, please have the following ready:\n")); + + console.log(chalk.whiteBright("1. Stripe Test API Key (sk_test_...)")); + console.log( + chalk.gray(" → Used to link Stripe to your test account for payment processing\n"), + ); + + console.log(chalk.whiteBright("2. Upstash Redis REST URL and Token")); + console.log( + chalk.gray(" → Used for caching customer objects and testing race conditions\n"), + ); + + console.log(chalk.whiteBright("3. Tunnel URL (e.g., ngrok URL)")); + console.log( + chalk.gray( + " → Points to localhost:8080 so Stripe webhooks can reach your server\n", + ), + ); + + // Prompt user to continue + const { ready } = await inquirer.prompt([ + { + type: "confirm", + name: "ready", + message: chalk.cyan("Ready to begin setup?"), + default: true, + }, + ]); + + if (!ready) { + console.log(chalk.yellow("\nSetup cancelled. Run the script again when you're ready!\n")); + process.exit(0); + } +} + +async function main() { + // Show preparation checklist + await showPreparationChecklist(); + + try { + // Import db from server + const { db } = await import("../server/src/db/initDrizzle.js"); + + // Step 1: Create test organization in database and get API key + const autumnSecretKey = await createTestOrg({ db }); + + // Save org details immediately + updateMultipleEnvVars({ + TESTS_ORG: TEST_ORG_CONFIG.slug, + TESTS_ORG_ID: TEST_ORG_CONFIG.id, + ...(autumnSecretKey && { UNIT_TEST_AUTUMN_SECRET_KEY: autumnSecretKey }), + }); + + // Step 2: Get Stripe test key + const stripeTestKey = await setupStripeTestKey(); + + // Save Stripe key immediately + updateSingleEnvVar({ key: "STRIPE_TEST_KEY", value: stripeTestKey }); + + // Step 3: Get Upstash configuration + const { upstashUrl, upstashToken } = await setupUpstash(); + + // Save Upstash credentials immediately + updateMultipleEnvVars({ + UPSTASH_REDIS_REST_URL: upstashUrl, + UPSTASH_REDIS_REST_TOKEN: upstashToken, + }); + + // Step 4: Get tunnel URL + const tunnelUrl = await setupTunnelUrl(); + + // Save tunnel URL immediately + updateSingleEnvVar({ key: "STRIPE_WEBHOOK_URL", value: tunnelUrl }); + + // Step 5: Final update to ensure proper formatting + updateEnvFile({ + testOrgSlug: TEST_ORG_CONFIG.slug, + testOrgId: TEST_ORG_CONFIG.id, + autumnSecretKey, + stripeTestKey, + upstashUrl, + upstashToken, + tunnelUrl, + }); + + console.log( + chalk.magentaBright( + "\n================ Setup Complete! ================\n", + ), + ); + console.log(chalk.greenBright("🎉 Test organization setup complete! 🎉\n")); + console.log(chalk.cyan("Test Organization Details:")); + console.log(chalk.whiteBright(` Organization: ${TEST_ORG_CONFIG.slug}`)); + console.log(chalk.whiteBright(` ID: ${TEST_ORG_CONFIG.id}`)); + + if (autumnSecretKey) { + console.log(chalk.whiteBright(` Secret Key: ${autumnSecretKey}\n`)); + } else { + console.log( + chalk.whiteBright(" Secret Key: (using existing key from .env)\n"), + ); + } + + console.log(chalk.cyan("Next steps:")); + console.log( + chalk.whiteBright("1. Start your tunnel (e.g., ngrok http 8080)"), + ); + console.log(chalk.whiteBright("2. Start your development server")); + console.log( + chalk.whiteBright("3. Run tests with your new test organization!\n"), + ); + + process.exit(0); + } catch (error) { + console.error( + chalk.red("\n❌ Setup failed:"), + error instanceof Error ? error.message : error, + ); + process.exit(1); + } +} + +main(); diff --git a/scripts/setupTestUtils/createTestOrg.ts b/scripts/setupTestUtils/createTestOrg.ts new file mode 100644 index 000000000..f2a62e852 --- /dev/null +++ b/scripts/setupTestUtils/createTestOrg.ts @@ -0,0 +1,128 @@ +import { type OrgConfig, member, organizations, user } from "@autumn/shared"; +import chalk from "chalk"; +import { eq } from "drizzle-orm"; +import type { DrizzleCli } from "@server/db/initDrizzle.js"; +import { createKey } from "@server/internal/dev/api-keys/apiKeyUtils.js"; + +const TEST_ORG_CONFIG = { + id: "org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt", + slug: "unit-test-org", + name: "Unit Test Org", + createdAt: new Date(1738583937426).toISOString(), + created_at: 1738583937426, +}; + +/** + * Creates a test organization in the database and generates an API key + */ +export async function createTestOrg({ + db, +}: { + db: DrizzleCli; +}): Promise { + console.log( + chalk.magentaBright( + "\n================ Creating Test Organization ================\n", + ), + ); + + // Check if org already exists + const existingOrg = await db.query.organizations.findFirst({ + where: eq(organizations.id, TEST_ORG_CONFIG.id), + }); + + if (existingOrg) { + console.log( + chalk.yellowBright( + `Test organization '${TEST_ORG_CONFIG.slug}' already exists. Creating new API key.`, + ), + ); + + // Always create a new API key for existing org + const apiKey = await createKey({ + db, + env: "sandbox" as any, + name: "Unit Test Key", + orgId: TEST_ORG_CONFIG.id, + prefix: "am_sk_test", + meta: { + createdBy: "setup-test-script", + createdAt: new Date().toISOString(), + }, + userId: undefined, + }); + + console.log(chalk.greenBright("✅ Created API key for existing organization")); + return apiKey; + } + + // Create the test organization + const org = { + id: TEST_ORG_CONFIG.id, + slug: TEST_ORG_CONFIG.slug, + name: TEST_ORG_CONFIG.name, + createdAt: new Date(TEST_ORG_CONFIG.created_at), + created_at: TEST_ORG_CONFIG.created_at, + stripe_connected: false, + default_currency: "usd", + config: {} as OrgConfig, + onboarded: true, + }; + + await db.insert(organizations).values(org); + + console.log( + chalk.greenBright( + `✅ Created test organization: ${TEST_ORG_CONFIG.slug} (${TEST_ORG_CONFIG.id})`, + ), + ); + + // Get first 5 users from database and create memberships + const users = await db.select().from(user).limit(5); + + if (users.length > 0) { + const { generateId } = await import("@server/utils/genUtils.js"); + + const memberships = users.map((u) => ({ + id: generateId("mem"), + organizationId: TEST_ORG_CONFIG.id, + userId: u.id, + role: "owner", + createdAt: new Date(), + })); + + await db.insert(member).values(memberships); + + console.log( + chalk.greenBright( + `✅ Created ${memberships.length} membership(s) for test organization`, + ), + ); + } else { + console.log( + chalk.yellowBright( + "⚠ No users found in database. Skipping membership creation.", + ), + ); + } + + // Create API key for the new org + const apiKey = await createKey({ + db, + env: "sandbox" as any, + name: "Unit Test Key", + orgId: TEST_ORG_CONFIG.id, + prefix: "am_sk_test", + meta: { + createdBy: "setup-test-script", + createdAt: new Date().toISOString(), + }, + userId: undefined, + }); + + console.log(chalk.greenBright("✅ Created API key for test organization")); + + return apiKey; +} + +export { TEST_ORG_CONFIG }; diff --git a/scripts/setupTestUtils/incrementalEnvUpdate.ts b/scripts/setupTestUtils/incrementalEnvUpdate.ts new file mode 100644 index 000000000..a14d78dfd --- /dev/null +++ b/scripts/setupTestUtils/incrementalEnvUpdate.ts @@ -0,0 +1,87 @@ +import { readFileSync, writeFileSync } from "node:fs"; +import chalk from "chalk"; +import { envPath } from "./updateEnvFile.js"; + +/** + * Incrementally updates a single env variable in the .env file + */ +export function updateSingleEnvVar({ + key, + value, +}: { + key: string; + value: string; +}) { + try { + const envContent = readFileSync(envPath, "utf-8"); + const lines = envContent.split("\n"); + + // Check if the key already exists + let found = false; + const updatedLines = lines.map((line) => { + const trimmed = line.trim(); + if (trimmed.startsWith(`${key}=`)) { + found = true; + return `${key}=${value}`; + } + return line; + }); + + // If not found, add it to the end + if (!found) { + updatedLines.push(`${key}=${value}`); + } + + writeFileSync(envPath, updatedLines.join("\n")); + console.log(chalk.gray(` ✓ Saved ${key} to .env`)); + } catch (error) { + console.log( + chalk.red( + ` ⚠ Warning: Could not save ${key} to .env. You may need to add it manually.`, + ), + ); + } +} + +/** + * Updates multiple env variables at once + */ +export function updateMultipleEnvVars(vars: Record) { + try { + const envContent = readFileSync(envPath, "utf-8"); + const lines = envContent.split("\n"); + const keysToUpdate = Object.keys(vars); + const foundKeys = new Set(); + + // Update existing keys + const updatedLines = lines.map((line) => { + const trimmed = line.trim(); + for (const key of keysToUpdate) { + if (trimmed.startsWith(`${key}=`)) { + foundKeys.add(key); + return `${key}=${vars[key]}`; + } + } + return line; + }); + + // Add new keys that weren't found + for (const key of keysToUpdate) { + if (!foundKeys.has(key)) { + updatedLines.push(`${key}=${vars[key]}`); + } + } + + writeFileSync(envPath, updatedLines.join("\n")); + + for (const key of keysToUpdate) { + console.log(chalk.gray(` ✓ Saved ${key} to .env`)); + } + } catch (error) { + console.log( + chalk.red( + " ⚠ Warning: Could not save variables to .env. You may need to add them manually.", + ), + ); + } +} diff --git a/scripts/setupTestUtils/setupPrompts.ts b/scripts/setupTestUtils/setupPrompts.ts new file mode 100644 index 000000000..eb73214db --- /dev/null +++ b/scripts/setupTestUtils/setupPrompts.ts @@ -0,0 +1,136 @@ +import inquirer from "inquirer"; +import chalk from "chalk"; + +/** + * Prompts user for Stripe test API key + */ +export async function setupStripeTestKey(): Promise { + console.log( + chalk.magentaBright( + "\n================ Stripe Test API Key Setup ================\n", + ), + ); + console.log( + chalk.cyan( + "This Stripe test API key will be used to link Stripe to your test account.", + ), + ); + console.log( + chalk.cyan( + "You can find this in your Stripe Dashboard under Developers > API Keys (Test Mode).\n", + ), + ); + + const { stripeTestKey } = await inquirer.prompt([ + { + type: "input", + name: "stripeTestKey", + message: chalk.cyan("Enter your Stripe test secret key (sk_test_...):"), + validate: (input: string) => { + if (!input || input.length < 10) { + return "Please enter a valid Stripe test key"; + } + if (!input.startsWith("sk_test_")) { + return "Stripe test keys should start with 'sk_test_'"; + } + return true; + }, + }, + ]); + + return stripeTestKey; +} + +/** + * Prompts user for Upstash configuration + */ +export async function setupUpstash(): Promise<{ + upstashUrl: string; + upstashToken: string; +}> { + console.log( + chalk.magentaBright("\n================ Upstash Setup ================\n"), + ); + console.log( + chalk.cyan( + "Upstash is used for caching the customer object and is important for testing race conditions.", + ), + ); + console.log( + chalk.cyan( + "You can create a free Upstash Redis instance at https://upstash.com/\n", + ), + ); + + const { upstashUrl } = await inquirer.prompt([ + { + type: "input", + name: "upstashUrl", + message: chalk.cyan("Enter your Upstash Redis REST URL:"), + validate: (input: string) => { + if (!input || input.length < 10) { + return "Please enter a valid Upstash URL"; + } + if (!input.startsWith("https://")) { + return "Upstash URL should start with 'https://'"; + } + return true; + }, + }, + ]); + + const { upstashToken } = await inquirer.prompt([ + { + type: "input", + name: "upstashToken", + message: chalk.cyan("Enter your Upstash Redis REST token:"), + validate: (input: string) => { + if (!input || input.length < 10) { + return "Please enter a valid Upstash token"; + } + return true; + }, + }, + ]); + + return { upstashUrl, upstashToken }; +} + +/** + * Prompts user for tunnel URL + */ +export async function setupTunnelUrl(): Promise { + console.log( + chalk.magentaBright( + "\n================ Tunnel URL Setup ================\n", + ), + ); + console.log( + chalk.cyan( + "You need a tunnel that points to localhost:8080 (your server URL) to receive Stripe webhooks.", + ), + ); + console.log( + chalk.cyan("You can use tools like ngrok, localtunnel, or Cloudflare Tunnel."), + ); + console.log(chalk.cyan("Example: https://your-subdomain.ngrok.io\n")); + + const { tunnelUrl } = await inquirer.prompt([ + { + type: "input", + name: "tunnelUrl", + message: chalk.cyan("Enter your tunnel URL:"), + validate: (input: string) => { + if (!input || input.length < 10) { + return "Please enter a valid tunnel URL"; + } + if (!input.startsWith("https://") && !input.startsWith("http://")) { + return "Tunnel URL should start with 'http://' or 'https://'"; + } + return true; + }, + }, + ]); + + return tunnelUrl; +} diff --git a/scripts/setupTestUtils/updateEnvFile.ts b/scripts/setupTestUtils/updateEnvFile.ts new file mode 100644 index 000000000..e34af0215 --- /dev/null +++ b/scripts/setupTestUtils/updateEnvFile.ts @@ -0,0 +1,183 @@ +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import chalk from "chalk"; +import { config } from "dotenv"; + +// Get the directory of this script file +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** + * Find server/.env file robustly - works whether running from root or scripts dir + */ +function findEnvPath(): string { + // Try from script directory (scripts/setup-test.ts -> ../server/.env) + const fromScriptDir = resolve(__dirname, "../../server/.env"); + if (existsSync(fromScriptDir)) { + return fromScriptDir; + } + + // Try from current working directory + const fromCwd = resolve(process.cwd(), "server/.env"); + if (existsSync(fromCwd)) { + return fromCwd; + } + + // If neither exists, return the path from script dir (will fail later with clear error) + return fromScriptDir; +} + +export const envPath = findEnvPath(); + +// Load existing env vars +config({ path: envPath }); + +/** + * Updates server/.env with new test configuration + */ +export function updateEnvFile({ + testOrgSlug, + testOrgId, + autumnSecretKey, + stripeTestKey, + upstashUrl, + upstashToken, + tunnelUrl, +}: { + testOrgSlug: string; + testOrgId: string; + autumnSecretKey: string | null; + stripeTestKey: string; + upstashUrl: string; + upstashToken: string; + tunnelUrl: string; +}) { + console.log( + chalk.magentaBright( + "\n================ Updating Environment Variables ================\n", + ), + ); + + // Read existing .env file + let envContent = ""; + try { + envContent = readFileSync(envPath, "utf-8"); + } catch { + console.log( + chalk.red( + `❌ Could not read server/.env file at ${envPath}. Make sure it exists.`, + ), + ); + process.exit(1); + } + + // Parse existing env vars + const envVars = new Map(); + const lines = envContent.split("\n"); + + for (const line of lines) { + const trimmed = line.trim(); + // Skip empty lines and comments + if (!trimmed || trimmed.startsWith("#")) { + continue; + } + + const eqIndex = trimmed.indexOf("="); + if (eqIndex > 0) { + const key = trimmed.substring(0, eqIndex); + const value = trimmed.substring(eqIndex + 1); + envVars.set(key, value); + } + } + + // Update with new test variables + envVars.set("TESTS_ORG", testOrgSlug); + envVars.set("TESTS_ORG_ID", testOrgId); + + // Only update the secret key if a new one was generated + if (autumnSecretKey) { + envVars.set("UNIT_TEST_AUTUMN_SECRET_KEY", autumnSecretKey); + } + + envVars.set("STRIPE_TEST_KEY", stripeTestKey); + envVars.set("UPSTASH_REDIS_REST_URL", upstashUrl); + envVars.set("UPSTASH_REDIS_REST_TOKEN", upstashToken); + envVars.set("STRIPE_WEBHOOK_URL", tunnelUrl); + + // Build new env content, preserving structure + const sections: string[][] = []; + let currentSection: string[] = []; + let inTestSection = false; + + for (const line of lines) { + const trimmed = line.trim(); + + // Check if this is a section header + if (trimmed.startsWith("#")) { + if (currentSection.length > 0) { + sections.push(currentSection); + currentSection = []; + } + currentSection.push(line); + inTestSection = trimmed.toLowerCase().includes("test"); + continue; + } + + // Skip test-related vars from existing content - we'll add them fresh + if ( + trimmed.startsWith("TESTS_ORG") || + trimmed.startsWith("UNIT_TEST_AUTUMN_SECRET_KEY") || + trimmed.startsWith("STRIPE_TEST_KEY") || + trimmed.startsWith("UPSTASH_REDIS_REST") || + (trimmed.startsWith("STRIPE_WEBHOOK_URL") && inTestSection) + ) { + continue; + } + + currentSection.push(line); + } + + if (currentSection.length > 0) { + sections.push(currentSection); + } + + // Add test configuration section + const testSection = [ + "", + "# Test Configuration", + `TESTS_ORG=${testOrgSlug}`, + `TESTS_ORG_ID=${testOrgId}`, + ]; + + // Only add secret key if it was generated/updated + if (autumnSecretKey) { + testSection.push(`UNIT_TEST_AUTUMN_SECRET_KEY=${autumnSecretKey}`); + } else if (envVars.has("UNIT_TEST_AUTUMN_SECRET_KEY")) { + testSection.push( + `UNIT_TEST_AUTUMN_SECRET_KEY=${envVars.get("UNIT_TEST_AUTUMN_SECRET_KEY")}`, + ); + } + + testSection.push( + `STRIPE_TEST_KEY=${stripeTestKey}`, + "", + "# Upstash (for caching)", + `UPSTASH_REDIS_REST_URL=${upstashUrl}`, + `UPSTASH_REDIS_REST_TOKEN=${upstashToken}`, + "", + "# Tunnel URL (for Stripe webhooks)", + `STRIPE_WEBHOOK_URL=${tunnelUrl}`, + "", + ); + + sections.push(testSection); + + // Write back to file + const newContent = sections.map((s) => s.join("\n")).join("\n"); + writeFileSync(envPath, newContent); + + console.log( + chalk.greenBright(`✅ Environment variables updated in ${envPath}`), + ); +} diff --git a/scripts/test.ts b/scripts/test.ts new file mode 100644 index 000000000..be06646c3 --- /dev/null +++ b/scripts/test.ts @@ -0,0 +1,279 @@ +#!/usr/bin/env node +import { spawn } from "node:child_process"; +import { existsSync, readdirSync, statSync } from "node:fs"; +import { join, relative, resolve } from "node:path"; +import chalk from "chalk"; + +/** + * Recursively finds all test files in a directory + */ +function findTestFiles({ dir }: { dir: string }): string[] { + const files: string[] = []; + const entries = readdirSync(dir); + + for (const entry of entries) { + const fullPath = join(dir, entry); + const stat = statSync(fullPath); + + if (stat.isDirectory()) { + files.push(...findTestFiles({ dir: fullPath })); + } else if ( + entry.endsWith(".ts") && + !entry.includes("Utils") && + !entry.includes("utils") + ) { + files.push(fullPath); + } + } + + return files; +} + +/** + * Fuzzy matches a search term against test file paths and returns a score + */ +function fuzzyMatchScore({ + search, + filePath, +}: { + search: string; + filePath: string; +}): number { + const searchLower = search.toLowerCase(); + const pathLower = filePath.toLowerCase(); + const fileName = pathLower.split("/").pop() || ""; + + // Check if search matches exactly in filename (highest priority) + if (fileName === `${searchLower}.ts`) { + return 1000; + } + + // Check if filename starts with search (high priority) + if (fileName.startsWith(searchLower)) { + return 500; + } + + // Check if search is contained in filename + if (fileName.includes(searchLower)) { + return 100; + } + + // Simple fuzzy match - check if all characters appear in order + let searchIndex = 0; + let score = 0; + for ( + let i = 0; + i < pathLower.length && searchIndex < searchLower.length; + i++ + ) { + if (pathLower[i] === searchLower[searchIndex]) { + searchIndex++; + score++; + } + } + + // Return 0 if not all characters matched + if (searchIndex !== searchLower.length) { + return 0; + } + + return score; +} + +/** + * Runs shell test scripts from server/shell/ directory or individual test files + */ +async function runTest() { + const scriptName = process.argv[2]; + const additionalArgs = process.argv.slice(3); + + if (!scriptName) { + console.log( + chalk.red("❌ Please provide a shell script or test file name"), + ); + console.log( + chalk.cyan("\nUsage: bun tests [args...]"), + ); + console.log(chalk.gray("Examples:")); + console.log(chalk.gray(" bun tests g1")); + console.log(chalk.gray(" bun tests g1 setup")); + console.log( + chalk.gray(" bun tests basic1 # fuzzy matches test file"), + ); + console.log( + chalk.gray(" bun tests attach/basic1 # matches path pattern\n"), + ); + process.exit(1); + } + + const serverDir = resolve(process.cwd(), "server"); + const shellScript = resolve(serverDir, "shell", `${scriptName}.sh`); + + // First try to find a shell script + if (existsSync(shellScript)) { + const argsDisplay = + additionalArgs.length > 0 ? ` ${additionalArgs.join(" ")}` : ""; + console.log( + chalk.cyan(`🧪 Running shell script: ${scriptName}.sh${argsDisplay}\n`), + ); + + // Create a new process group by spawning with detached: true + const child = spawn("bash", [shellScript, ...additionalArgs], { + cwd: serverDir, + stdio: "inherit", + env: { ...process.env, NODE_ENV: "production" }, + detached: true, + }); + + // Store the process group ID + const pgid = child.pid; + + // Forward termination signals to entire process group + const killProcessGroup = () => { + if (pgid) { + try { + // Kill the entire process group with SIGKILL (force kill) + process.kill(-pgid, "SIGKILL"); + } catch (_err) { + // Process group might already be dead + } + } + }; + + process.on("SIGINT", () => { + console.log(chalk.yellow("\n⚠️ Received SIGINT, killing all test processes...\n")); + killProcessGroup(); + process.exit(130); + }); + + process.on("SIGTERM", () => { + console.log(chalk.yellow("\n⚠️ Received SIGTERM, killing all test processes...\n")); + killProcessGroup(); + process.exit(143); + }); + + process.on("exit", () => { + killProcessGroup(); + }); + + child.on("exit", (code) => { + if (code === 0) { + console.log( + chalk.green(`\n✅ Test ${scriptName} completed successfully`), + ); + } else { + console.log( + chalk.red(`\n❌ Test ${scriptName} failed with code ${code}`), + ); + process.exit(code || 1); + } + }); + + child.on("error", (error) => { + console.log(chalk.red(`\n❌ Error running test: ${error.message}`)); + process.exit(1); + }); + return; + } + + // If not a shell script, try fuzzy matching test files + console.log( + chalk.cyan(`🔍 Searching for test file matching: ${scriptName}\n`), + ); + + const testsDir = resolve(serverDir, "tests"); + const allTestFiles = findTestFiles({ dir: testsDir }); + + // Find matches with scores + const matches = allTestFiles + .map((file) => ({ + path: file, + relative: relative(serverDir, file), + score: fuzzyMatchScore({ search: scriptName, filePath: file }), + })) + .filter((match) => match.score > 0) + .sort((a, b) => b.score - a.score); + + if (matches.length === 0) { + console.log(chalk.red(`❌ No test file found matching: ${scriptName}`)); + console.log(chalk.gray(` Searched in: ${testsDir}\n`)); + process.exit(1); + } + + const bestMatch = matches[0]; + const otherMatches = matches.slice(1, 5); + + if (otherMatches.length > 0) { + console.log( + chalk.yellow(`⚠️ Multiple matches found, picking best match:\n`), + ); + console.log(chalk.green(` ✓ ${bestMatch.relative} (selected)`)); + for (const match of otherMatches) { + console.log(chalk.gray(` ${match.relative}`)); + } + console.log(); + } + + const testFile = bestMatch; + console.log(chalk.green(`✓ Found: ${testFile.relative}\n`)); + console.log(chalk.cyan(`🧪 Running test file...\n`)); + + // Run the test file with mocha + const child = spawn("bunx", ["mocha", "--timeout", "0", testFile.path], { + cwd: serverDir, + stdio: "inherit", + env: { ...process.env, NODE_ENV: "production" }, + detached: true, + }); + + // Store the process group ID + const pgid = child.pid; + + // Forward termination signals to entire process group + const killProcessGroup = () => { + if (pgid) { + try { + // Kill the entire process group with SIGKILL (force kill) + process.kill(-pgid, "SIGKILL"); + } catch (_err) { + // Process group might already be dead + } + } + }; + + process.on("SIGINT", () => { + console.log(chalk.yellow("\n⚠️ Received SIGINT, killing test process...\n")); + killProcessGroup(); + process.exit(130); + }); + + process.on("SIGTERM", () => { + console.log(chalk.yellow("\n⚠️ Received SIGTERM, killing test process...\n")); + killProcessGroup(); + process.exit(143); + }); + + process.on("exit", () => { + killProcessGroup(); + }); + + child.on("exit", (code) => { + if (code === 0) { + console.log( + chalk.green(`\n✅ Test ${scriptName} completed successfully`), + ); + } else { + console.log( + chalk.red(`\n❌ Test ${scriptName} failed with code ${code}`), + ); + process.exit(code || 1); + } + }); + + child.on("error", (error) => { + console.log(chalk.red(`\n❌ Error running test: ${error.message}`)); + process.exit(1); + }); +} + +runTest(); diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 000000000..024ea09d3 --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "resolveJsonModule": true, + "allowJs": true, + "checkJs": false, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "types": ["node"], + "baseUrl": ".", + "paths": { + "@server/*": ["../server/src/*"], + "@shared/*": ["../shared/*"] + } + }, + "include": ["**/*.ts", "**/*.js"], + "exclude": ["node_modules", "dist"] +} diff --git a/server/package.json b/server/package.json index c721526e1..77d3c5702 100644 --- a/server/package.json +++ b/server/package.json @@ -24,6 +24,7 @@ "license": "Apache-2.0", "dependencies": { "@ai-sdk/anthropic": "^1.2.10", + "@amplitude/analytics-node": "^1.5.18", "@anthropic-ai/sdk": "^0.32.1", "@autumn/shared": "workspace:*", "@axiomhq/pino": "^1.3.1", @@ -95,7 +96,7 @@ "recaseai": "^0.0.37", "resend": "^4.1.1", "semver": "^7.7.2", - "stripe": "^18.4.0", + "stripe": "18.4.0-beta.2", "svix": "^1.45.1", "tsc-alias": "^1.8.16", "ws": "^8.18.0", diff --git a/server/register.ts b/server/register.ts new file mode 100644 index 000000000..13c15dd04 --- /dev/null +++ b/server/register.ts @@ -0,0 +1,31 @@ +import "dotenv/config"; +import Stripe from "stripe"; + +const main = async () => { + const stripe = new Stripe(process.env.STRIPE_SANDBOX_SECRET_KEY || ""); + + const result = await stripe.webhookEndpoints.create({ + url: "https://api.useautumn.com/webhooks/connect/sandbox", + enabled_events: [ + "checkout.session.completed", + "customer.subscription.created", + "customer.subscription.updated", + "customer.subscription.deleted", + "customer.discount.deleted", + "invoice.paid", + "invoice.upcoming", + "invoice.created", + "invoice.finalized", + "invoice.updated", + "subscription_schedule.canceled", + "subscription_schedule.updated", + ], + connect: true, + }); + + console.log(result); +}; + +main() + .catch(console.error) + .then(() => process.exit(0)); diff --git a/server/shell/config.sh b/server/shell/config.sh index 384ab7bfd..a6f94394e 100755 --- a/server/shell/config.sh +++ b/server/shell/config.sh @@ -1,4 +1,4 @@ #!/bin/bash -MOCHA_SETUP="bunx mocha tests/00_setup.ts" +MOCHA_SETUP="bunx mocha --timeout 10000000 tests/00_setup.ts" MOCHA_CMD="bunx mocha --parallel -j 6 --timeout 10000000 --ignore tests/00_setup.ts" \ No newline at end of file diff --git a/server/shell/g5.sh b/server/shell/g5.sh index 4eb6760ba..f083cf29f 100755 --- a/server/shell/g5.sh +++ b/server/shell/g5.sh @@ -7,20 +7,19 @@ source "$(dirname "$0")/config.sh" if [[ "$1" == *"setup"* ]]; then MOCHA_PARALLEL=true $MOCHA_SETUP fi -# $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/referrals/paid/*.ts' \ -# 'tests/advanced/rollovers/*.ts' \ -# 'tests/advanced/customInterval/*.ts' +$MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \ + 'tests/advanced/coupons/*.ts' \ + 'tests/attach/updateQuantity/*.ts' \ + 'tests/advanced/referrals/*.ts' \ + 'tests/advanced/referrals/paid/*.ts' \ + 'tests/advanced/rollovers/*.ts' \ + 'tests/advanced/customInterval/*.ts' -# $MOCHA_CMD 'tests/attach/multiProduct/*.ts' \ -# 'tests/advanced/usageLimit/*.ts' +$MOCHA_CMD 'tests/attach/multiProduct/*.ts' \ + 'tests/advanced/usageLimit/*.ts' -# $MOCHA_CMD 'tests/advanced/usage/*.ts' +$MOCHA_CMD 'tests/advanced/usage/*.ts' \ No newline at end of file diff --git a/server/src/check.ts b/server/src/check.ts index 51fa2a239..27f711a96 100644 --- a/server/src/check.ts +++ b/server/src/check.ts @@ -15,8 +15,8 @@ import { import type Stripe from "stripe"; import { initDrizzle } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { getStripeSchedules } from "@/external/stripe/stripeSubUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { createSupabaseClient } from "@/external/supabaseUtils.js"; import { CusService } from "@/internal/customers/CusService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; diff --git a/server/src/cron/cronUtils.ts b/server/src/cron/cronUtils.ts index 2a1f6ae8f..b773ef2cb 100644 --- a/server/src/cron/cronUtils.ts +++ b/server/src/cron/cronUtils.ts @@ -12,7 +12,7 @@ import { UTCDate } from "@date-fns/utc"; import chalk from "chalk"; import { format, getDate, getMonth, setDate } from "date-fns"; import { Decimal } from "decimal.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; diff --git a/server/src/external/connect/connectUtils.ts b/server/src/external/connect/connectUtils.ts new file mode 100644 index 000000000..fbdff6b27 --- /dev/null +++ b/server/src/external/connect/connectUtils.ts @@ -0,0 +1,121 @@ +import { AppEnv, InternalError, type Organization } from "@autumn/shared"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { decryptData } from "@/utils/encryptUtils.js"; +import type { Logger } from "../logtail/logtailUtils.js"; +import { initMasterStripe } from "./initStripeCli.js"; + +export const orgToAccountId = ({ + org, + env, + noDefaultAccount = false, +}: { + org: Organization; + env: AppEnv; + noDefaultAccount?: boolean; +}): string | undefined => { + if (env === AppEnv.Sandbox) { + const config = org.test_stripe_connect; + if (noDefaultAccount) { + return config?.account_id; + } + return config?.account_id || config?.default_account_id; + } else { + return org.live_stripe_connect?.account_id; + } +}; + +export const deauthorizeAccount = async ({ + accountId, + env, + logger, +}: { + accountId: string; + env: AppEnv; + logger: Logger; +}) => { + // OAuth-connected accounts must be deauthorized, not deleted + // Platform-managed accounts can be deleted + + const masterStripe = initMasterStripe({ env }); + try { + await masterStripe.oauth.deauthorize({ + client_id: + env === AppEnv.Live + ? process.env.STRIPE_LIVE_CLIENT_ID || "" + : process.env.STRIPE_SANDBOX_CLIENT_ID || "", + stripe_user_id: accountId, + }); + logger.info(`Deauthorized account ${accountId} for ${env}`); + } catch (error) { + // If deauthorization fails, the account might have already been disconnected + // or it's a platform-managed account that needs to be deleted + logger.error("Failed to deauthorize account, attempting deletion:", error); + } +}; + +export const deleteConnectedAccount = async ({ + accountId, + env, + logger, +}: { + accountId: string; + env: AppEnv; + logger: Logger; +}) => { + const masterStripe = initMasterStripe({ env }); + try { + await masterStripe.accounts.del(accountId); + logger.info(`Deleted account ${accountId} for ${env}`); + } catch (error) { + logger.error(`Failed to delete account ${accountId} for ${env}`, error); + } +}; + +export const shouldUseMaster = ({ + org, + env, +}: { + org: Organization; + env: AppEnv; +}) => { + const useMasterOrg = + env === AppEnv.Sandbox + ? Boolean(org.test_stripe_connect?.master_org_id) && + Boolean(org.test_stripe_connect?.account_id) + : Boolean(org.live_stripe_connect?.master_org_id) && + Boolean(org.live_stripe_connect?.account_id); + + if (useMasterOrg && !org.master) { + throw new InternalError({ + message: `Master organization not found for ${env} org ${org.id}`, + }); + } + + if (!useMasterOrg) return false; + + return useMasterOrg; +}; + +export const getConnectWebhookSecret = async ({ + db, + orgId, + env, +}: { + db: DrizzleCli; + orgId: string; + env: AppEnv; +}) => { + const org = await OrgService.get({ db, orgId }); + const prefix = env === AppEnv.Sandbox ? "test" : "live"; + const secret = org.stripe_config?.[`${prefix}_connect_webhook_secret`]; + + if (!secret) { + throw new InternalError({ + message: `Connect webhook secret not found for ${env} org ${orgId}`, + }); + } + + const decrypted = decryptData(secret); + return decrypted; +}; diff --git a/server/src/external/connect/createStripeCli.ts b/server/src/external/connect/createStripeCli.ts new file mode 100644 index 000000000..93fd1ec7c --- /dev/null +++ b/server/src/external/connect/createStripeCli.ts @@ -0,0 +1,71 @@ +import { + AppEnv, + ErrCode, + InternalError, + type Organization, + RecaseError, +} from "@autumn/shared"; +import Stripe from "stripe"; +import { isStripeConnected } from "@/internal/orgs/orgUtils.js"; +import { decryptData } from "@/utils/encryptUtils.js"; +import { orgToAccountId, shouldUseMaster } from "./connectUtils.js"; +import { initMasterStripe, initPlatformStripe } from "./initStripeCli.js"; + +export const createStripeCli = ({ + org, + env, + legacyVersion, + throughSecretKey = false, +}: { + org: Organization; + env: AppEnv; + legacyVersion?: boolean; + throughSecretKey?: boolean; +}) => { + // Try secret key first. + if (isStripeConnected({ org, env, throughSecretKey: true })) { + // Secret key flow + const encrypted = + env === AppEnv.Sandbox + ? org.stripe_config?.test_api_key + : org.stripe_config?.live_api_key; + + if (!encrypted) { + throw new RecaseError({ + message: `Please connect your Stripe ${env === AppEnv.Sandbox ? "test" : "live"} secret key. You can find it here: https://dashboard.stripe.com${env === AppEnv.Sandbox ? "/test" : ""}/apikeys`, + code: ErrCode.StripeConfigNotFound, + statusCode: 400, + }); + } + + const decrypted = decryptData(encrypted); + return new Stripe(decrypted, { + apiVersion: legacyVersion + ? ("2025-02-24.acacia" as any) + : "2025-07-30.basil", + }); + } + + // Then try account ID + const accountId = orgToAccountId({ org, env }); + + if (accountId && !throughSecretKey) { + // Check if this org has a master_org_id (platform flow) + const useMaster = shouldUseMaster({ org, env }); + if (useMaster) { + return initPlatformStripe({ + masterOrg: org.master, + env, + accountId, + legacyVersion, + }); + } + + // Standard flow - use Autumn's master Stripe keys + return initMasterStripe({ accountId, legacyVersion, env }); + } + + throw new InternalError({ + message: `No stripe account linked to organization ${org.id}`, + }); +}; diff --git a/server/src/external/connect/initStripeCli.ts b/server/src/external/connect/initStripeCli.ts new file mode 100644 index 000000000..ee62a5edc --- /dev/null +++ b/server/src/external/connect/initStripeCli.ts @@ -0,0 +1,119 @@ +import { + AppEnv, + InternalError, + type Organization, + RecaseError, +} from "@autumn/shared"; +import { decryptData } from "@/utils/encryptUtils.js"; +import "dotenv/config"; +import Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { getConnectWebhookSecret } from "./connectUtils.js"; + +export const initMasterStripe = (params?: { + accountId?: string; + legacyVersion?: boolean; + env?: AppEnv; +}) => { + let secretKey: string; + + if (params?.env === AppEnv.Live) { + if (!process.env.STRIPE_LIVE_SECRET_KEY) { + throw new InternalError({ + message: "STRIPE_LIVE_SECRET_KEY env variable is not set", + }); + } + secretKey = process.env.STRIPE_LIVE_SECRET_KEY; + } else { + if (!process.env.STRIPE_SANDBOX_SECRET_KEY) { + throw new InternalError({ + message: "STRIPE_SANDBOX_SECRET_KEY env variable is not set", + }); + } + secretKey = process.env.STRIPE_SANDBOX_SECRET_KEY; + } + + // if (!params) { + // return new Stripe(secretKey); + // } + + return new Stripe(secretKey, { + stripeAccount: params?.accountId, + apiVersion: params?.legacyVersion + ? ("2025-02-24.acacia" as any) + : undefined, + }); +}; + +export const initPlatformStripe = ({ + masterOrg, + env, + accountId, + legacyVersion, +}: { + masterOrg: Organization | null; + env: AppEnv; + accountId?: string; + legacyVersion?: boolean; +}) => { + if (!masterOrg) { + throw new InternalError({ + message: "Master organization is undefined in initPlatformStripe", + }); + } + + // Get master org's secret key and validate access to the account + const encrypted = + env === AppEnv.Sandbox + ? masterOrg.stripe_config?.test_api_key + : masterOrg.stripe_config?.live_api_key; + + if (!encrypted) { + const envLabel = env === AppEnv.Sandbox ? "test" : "live"; + throw new RecaseError({ + message: `Master organization must have Stripe ${envLabel} secret key connected`, + }); + } + + const decrypted = decryptData(encrypted); + if (!decrypted) { + throw new InternalError({ + message: `Failed to decrypt master organization's Stripe secret key`, + }); + } + + return new Stripe(decrypted, { + stripeAccount: accountId || undefined, + apiVersion: legacyVersion ? ("2025-02-24.acacia" as any) : undefined, + }); +}; + +export const getStripeWebhookSecret = async ({ + db, + orgId, + env, +}: { + db: DrizzleCli; + orgId?: string; + env: AppEnv; +}) => { + // If org ID... + if (orgId) { + return await getConnectWebhookSecret({ db, orgId, env }); + } + + let secret: string; + if (env === AppEnv.Live) { + secret = process.env.STRIPE_LIVE_WEBHOOK_SECRET || ""; + } else { + secret = process.env.STRIPE_SANDBOX_WEBHOOK_SECRET || ""; + } + + if (!secret) { + throw new InternalError({ + message: `STRIPE_WEBHOOK_SECRET env variable is not set (${env})`, + }); + } + + return secret; +}; diff --git a/server/src/external/connect/registerConnectWebhook.ts b/server/src/external/connect/registerConnectWebhook.ts new file mode 100644 index 000000000..1b794e733 --- /dev/null +++ b/server/src/external/connect/registerConnectWebhook.ts @@ -0,0 +1,44 @@ +import type Stripe from "stripe"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { WEBHOOK_EVENTS } from "@/utils/constants.js"; +import { encryptData } from "@/utils/encryptUtils.js"; +import { initPlatformStripe } from "./initStripeCli.js"; + +export const registerConnectWebhook = async ({ + ctx, +}: { + ctx: AutumnContext; +}) => { + const { db, org, env, logger } = ctx; + // Init master stripe + const stripeCli = initPlatformStripe({ masterOrg: org, env }); + + const curWebhookEndpoints = await stripeCli.webhookEndpoints.list(); + const backendUrl = process.env.SERVER_URL || process.env.STRIPE_WEBHOOK_URL; + + const webhookUrl = `${backendUrl}/webhooks/connect/${env}?org_id=${org.id}`; + + if (curWebhookEndpoints.data.some((webhook) => webhook.url === webhookUrl)) + return; + + const webhook = await stripeCli.webhookEndpoints.create({ + url: webhookUrl, + enabled_events: + WEBHOOK_EVENTS as Stripe.WebhookEndpointCreateParams.EnabledEvent[], + connect: true, + }); + + logger.info(`Registered connect webhook for ${org.slug} ${env}`); + + await OrgService.updateConnectWebhookSecret({ + db, + orgId: org.id, + env, + secret: encryptData(webhook.secret as string), + }); + + logger.info(`Updated connect webhook secret for ${org.slug} ${env}`); + + return webhook; +}; diff --git a/server/src/external/logtail/logtailUtils.ts b/server/src/external/logtail/logtailUtils.ts index 577888f73..2b58cc0f2 100644 --- a/server/src/external/logtail/logtailUtils.ts +++ b/server/src/external/logtail/logtailUtils.ts @@ -103,20 +103,5 @@ export const createLogger = () => { return createLoggerStructure(pinoLogger); }; -// export const createLogtailAll = () => { -// if ( -// !process.env.LOGTAIL_ALL_SOURCE_TOKEN || -// !process.env.LOGTAIL_ALL_INGESTING_HOST -// ) { -// return null; -// } - -// const logtail = new Logtail(process.env.LOGTAIL_ALL_SOURCE_TOKEN!, { -// endpoint: process.env.LOGTAIL_ALL_INGESTING_HOST!, -// }); - -// return logtail; -// }; - export const logger = createLogger(); export type Logger = ReturnType; diff --git a/server/src/external/posthog/createPosthogCli.ts b/server/src/external/posthog/createPosthogCli.ts deleted file mode 100644 index c74a02cb7..000000000 --- a/server/src/external/posthog/createPosthogCli.ts +++ /dev/null @@ -1,16 +0,0 @@ -import dotenv from "dotenv"; -dotenv.config(); - -import { PostHog } from "posthog-node"; -import { logger } from "../logtail/logtailUtils.js"; - -export const createPosthogCli = () => { - if (!process.env.POSTHOG_API_KEY) { - logger.warn("POSTHOG_API_KEY not set, skipping posthog"); - return null; - } - - return new PostHog(process.env.POSTHOG_API_KEY, { - host: process.env.POSTHOG_HOST_URL ?? "https://us.i.posthog.com", - }); -}; diff --git a/server/src/external/posthog/posthogCapture.ts b/server/src/external/posthog/posthogCapture.ts deleted file mode 100644 index 4a75378e5..000000000 --- a/server/src/external/posthog/posthogCapture.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { EventMessage, PostHog } from "posthog-node"; - -export const posthogCapture = ({ - posthog, - params, -}: { - posthog?: PostHog; - params: EventMessage; -}) => { - try { - if (process.env.NODE_ENV === "development" || !posthog) { - return; - } - - posthog.capture(params); - } catch (error) { - console.error("Failed to capture posthog event", params); - console.error(error); - } -}; diff --git a/server/src/external/redis/redisUtils.ts b/server/src/external/redis/redisUtils.ts index d8e210c77..bb48e1767 100644 --- a/server/src/external/redis/redisUtils.ts +++ b/server/src/external/redis/redisUtils.ts @@ -29,9 +29,9 @@ export const handleAttachRaceCondition = async ({ const originalJson = res.json; res.json = async function (body: any) { try { - await clearLock({ lockKey, logger: req.logtail }); + await clearLock({ lockKey, logger: req.logger }); } catch (error) { - req.logtail.warn("❗️❗️ Error clearing lock", { + req.logger.warn("❗️❗️ Error clearing lock", { error, }); } @@ -44,7 +44,7 @@ export const handleAttachRaceCondition = async ({ throw error; } - req.logtail.warn("❗️❗️ Error acquiring lock", { + req.logger.warn("❗️❗️ Error acquiring lock", { error, }); return null; diff --git a/server/src/external/resend/loopsUtils.ts b/server/src/external/resend/loopsUtils.ts index 33bdba40b..7ca658acc 100644 --- a/server/src/external/resend/loopsUtils.ts +++ b/server/src/external/resend/loopsUtils.ts @@ -1,6 +1,6 @@ +import type { User } from "better-auth"; import { LoopsClient } from "loops"; import { logger } from "../logtail/logtailUtils.js"; -import { User } from "better-auth"; const createLoopsCli = () => { return new LoopsClient(process.env.LOOPS_API_KEY || ""); @@ -10,9 +10,9 @@ export const createLoopsContact = async (user: User) => { if (!process.env.LOOPS_API_KEY) return; try { - let email = user.email; - let firstName = user.name?.split(" ")[0] || ""; - let lastName = user.name?.split(" ")[1] || ""; + const email = user.email; + const firstName = user.name?.split(" ")[0] || ""; + const lastName = user.name?.split(" ")[1] || ""; const loops = createLoopsCli(); const resp = await loops.createContact(email, { diff --git a/server/src/external/stripe/handleStripeWebhookEvent.ts b/server/src/external/stripe/handleStripeWebhookEvent.ts new file mode 100644 index 000000000..056a479ef --- /dev/null +++ b/server/src/external/stripe/handleStripeWebhookEvent.ts @@ -0,0 +1,273 @@ +import type { AppEnv, Organization } from "@autumn/shared"; +import chalk from "chalk"; +import { Stripe } from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js"; +import { unsetOrgStripeKeys } from "@/internal/orgs/orgUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import type { Logger } from "../logtail/logtailUtils.js"; +import { handleCheckoutSessionCompleted } from "./webhookHandlers/handleCheckoutCompleted.js"; +import { handleCusDiscountDeleted } from "./webhookHandlers/handleCusDiscountDeleted.js"; +import { handleInvoiceCreated } from "./webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.js"; +import { handleInvoiceFinalized } from "./webhookHandlers/handleInvoiceFinalized.js"; +import { handleInvoicePaid } from "./webhookHandlers/handleInvoicePaid.js"; +import { handleInvoiceUpdated } from "./webhookHandlers/handleInvoiceUpdated.js"; +import { handleSubCreated } from "./webhookHandlers/handleSubCreated.js"; +import { handleSubDeleted } from "./webhookHandlers/handleSubDeleted.js"; +import { handleSubscriptionScheduleCanceled } from "./webhookHandlers/handleSubScheduleCanceled.js"; +import { handleSubscriptionUpdated } from "./webhookHandlers/handleSubUpdated.js"; + +const logStripeWebhook = ({ + logger, + org, + event, +}: { + logger: Logger; + org: Organization; + event: Stripe.Event; +}) => { + logger.info( + `${chalk.yellow("STRIPE").padEnd(18)} ${event.type.padEnd(30)} ${org.slug} | ${event.id}`, + ); +}; + +const coreEvents = [ + "customer.subscription.created", + "customer.subscription.updated", + "customer.subscription.deleted", + "invoice.paid", + "invoice.created", + "invoice.finalized", + "subscription_schedule.canceled", + "checkout.session.completed", +]; + +const handleStripeWebhookRefresh = async ({ + eventType, + data, + db, + org, + env, + logger, +}: { + eventType: string; + data: any; + db: DrizzleCli; + org: Organization; + env: AppEnv; + logger: any; +}) => { + if (coreEvents.includes(eventType)) { + const stripeCusId = data.object.customer; + if (!stripeCusId) { + logger.warn( + `stripe webhook cache refresh, object doesn't contain customer id`, + { + data: { + eventType, + object: data.object, + }, + }, + ); + return; + } + + const cus = await CusService.getByStripeId({ + db, + stripeId: stripeCusId, + }); + + if (!cus) { + logger.warn( + `Searched for customer by stripe id, but not found: ${stripeCusId}`, + ); + return; + } + + await deleteCusCache({ + db, + customerId: cus.id!, + org, + env, + }); + } +}; + +/** + * Handles Stripe webhook events after org/env extraction + */ +export const handleStripeWebhookEvent = async ({ + event, + db, + org, + env, + logger, + req, +}: { + event: Stripe.Event; + db: DrizzleCli; + org: Organization; + env: AppEnv; + logger: Logger; + req: ExtendedRequest; +}) => { + logStripeWebhook({ logger, org, event }); + + try { + const stripeCli = createStripeCli({ org, env }); + switch (event.type) { + case "customer.subscription.created": + await handleSubCreated({ + db, + org, + subData: event.data.object, + env, + logger, + }); + break; + + case "customer.subscription.updated": { + const subscription = event.data.object; + await handleSubscriptionUpdated({ + req, + db, + org, + subscription, + previousAttributes: event.data.previous_attributes, + env, + logger, + }); + break; + } + + case "customer.subscription.deleted": + await handleSubDeleted({ + req, + stripeCli, + data: event.data.object, + logger, + }); + break; + + case "checkout.session.completed": { + const checkoutSession = event.data.object; + await handleCheckoutSessionCompleted({ + req, + db, + data: checkoutSession, + org, + env, + logger, + }); + break; + } + + case "invoice.paid": { + const invoice = event.data.object; + await handleInvoicePaid({ + db, + org, + invoiceData: invoice, + env, + event, + req, + }); + break; + } + + case "invoice.updated": + await handleInvoiceUpdated({ + stripeCli, + env, + event, + req, + }); + break; + + case "invoice.created": { + const createdInvoice = event.data.object; + await handleInvoiceCreated({ + db, + org, + data: createdInvoice, + env, + logger, + }); + break; + } + + case "invoice.finalized": { + const finalizedInvoice = event.data.object; + await handleInvoiceFinalized({ + db, + org, + data: finalizedInvoice, + env, + logger, + }); + break; + } + + case "subscription_schedule.canceled": { + const canceledSchedule = event.data.object; + await handleSubscriptionScheduleCanceled({ + db, + org, + env, + schedule: canceledSchedule, + logger, + }); + break; + } + + case "customer.discount.deleted": + await handleCusDiscountDeleted({ + db, + org, + discount: event.data.object, + env, + logger, + res: req, + }); + break; + } + } catch (error) { + if (error instanceof Stripe.errors.StripeError) { + if (error.message.includes("No such customer")) { + logger.warn(`stripe customer missing: ${error.message}`); + return { success: true }; + } + + if (error.message.includes("Expired API Key provided")) { + await unsetOrgStripeKeys({ + db, + org, + env, + }); + + return { success: true }; + } + } + + logger.error(`Stripe webhook, error: ${error}`, { error }); + throw error; + } + + try { + await handleStripeWebhookRefresh({ + eventType: event.type, + data: event.data, + db, + org, + env, + logger, + }); + } catch (error) { + logger.error(`Stripe webhook, error refreshing cache!`, { error }); + return { success: true }; + } + + return { success: true }; +}; diff --git a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts index 1c91bc26f..88d958a9e 100644 --- a/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts +++ b/server/src/external/stripe/stripeCouponUtils/stripeCouponUtils.ts @@ -12,9 +12,9 @@ import { RewardType, type UsagePriceConfig, } from "@autumn/shared"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { pricesOnlyOneOff } from "@/internal/products/prices/priceUtils.js"; import RecaseError from "@/utils/errorUtils.js"; -import { createStripeCli } from "../utils.js"; const couponToStripeDuration = ({ coupon, diff --git a/server/src/external/stripe/stripeCusUtils.ts b/server/src/external/stripe/stripeCusUtils.ts index fe1888a4d..50807860c 100644 --- a/server/src/external/stripe/stripeCusUtils.ts +++ b/server/src/external/stripe/stripeCusUtils.ts @@ -8,9 +8,9 @@ import { import { StatusCodes } from "http-status-codes"; import type { Stripe } from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { CusService } from "@/internal/customers/CusService.js"; import RecaseError from "@/utils/errorUtils.js"; -import { createStripeCli } from "./utils.js"; export const getStripeCus = async ({ stripeCli, diff --git a/server/src/external/stripe/stripeEnsureUtils.ts b/server/src/external/stripe/stripeEnsureUtils.ts index 58f1268c9..e024fa729 100644 --- a/server/src/external/stripe/stripeEnsureUtils.ts +++ b/server/src/external/stripe/stripeEnsureUtils.ts @@ -1,11 +1,10 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { Stripe } from "stripe"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { AppEnv, Organization, products } from "@autumn/shared"; +import type { AppEnv, Organization } from "@autumn/shared"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { initProductInStripe } from "@/internal/products/productUtils.js"; -import { OrgService } from "@/internal/orgs/OrgService.js"; -import { createStripeCli } from "./utils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; export async function ensureStripeProducts({ db, @@ -55,9 +54,9 @@ export async function ensureStripeProductsWithEnv({ const updatedOrg = await OrgService.get({ db, orgId: req.org.id }); const batchInit: Promise[] = []; - for (let fullProduct of fullProducts) { + for (const fullProduct of fullProducts) { const initProduct = async () => { - let existsInStripe = products.data.find( + const existsInStripe = products.data.find( (p) => p.id === fullProduct.processor?.id, ); diff --git a/server/src/external/stripe/stripePriceUtils.ts b/server/src/external/stripe/stripePriceUtils.ts index 5516cfd9b..dc3002574 100644 --- a/server/src/external/stripe/stripePriceUtils.ts +++ b/server/src/external/stripe/stripePriceUtils.ts @@ -26,7 +26,7 @@ export const billingIntervalToStripe = ({ }: { interval: BillingInterval; intervalCount?: number | null; -}): Stripe.PriceCreateParams.Recurring => { +}): Stripe.PriceCreateParams.Recurring | Record => { const finalCount = intervalCount ?? 1; switch (interval) { case BillingInterval.Week: @@ -55,7 +55,8 @@ export const billingIntervalToStripe = ({ interval_count: finalCount, }; default: - throw new Error(`billingIntervalToStripe: invalid interval ${interval}`); + // throw new Error(`billingIntervalToStripe: invalid interval ${interval}`); + return {}; } }; diff --git a/server/src/external/stripe/stripeProductUtils.ts b/server/src/external/stripe/stripeProductUtils.ts index 8b3ae9617..e82ddd223 100644 --- a/server/src/external/stripe/stripeProductUtils.ts +++ b/server/src/external/stripe/stripeProductUtils.ts @@ -5,8 +5,8 @@ import { type Product, } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import RecaseError from "@/utils/errorUtils.js"; -import { createStripeCli } from "./utils.js"; export const createStripeProduct = async ( org: Organization, diff --git a/server/src/external/stripe/stripeWebhooks.ts b/server/src/external/stripe/stripeWebhooks.ts index 7aa8687fd..d6551f6a6 100644 --- a/server/src/external/stripe/stripeWebhooks.ts +++ b/server/src/external/stripe/stripeWebhooks.ts @@ -1,53 +1,22 @@ -import express, { Router } from "express"; -import stripe, { Stripe } from "stripe"; -import chalk from "chalk"; - +import { AuthType, type Organization } from "@autumn/shared"; +import express, { type Router } from "express"; +import stripe, { type Stripe } from "stripe"; import { OrgService } from "@/internal/orgs/OrgService.js"; -import { AppEnv, AuthType, Organization } from "@autumn/shared"; - -import { handleCheckoutSessionCompleted } from "./webhookHandlers/handleCheckoutCompleted.js"; -import { handleSubscriptionUpdated } from "./webhookHandlers/handleSubUpdated.js"; -import { handleSubDeleted } from "./webhookHandlers/handleSubDeleted.js"; -import { handleSubCreated } from "./webhookHandlers/handleSubCreated.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"; -import { handleInvoiceFinalized } from "./webhookHandlers/handleInvoiceFinalized.js"; -import { handleSubscriptionScheduleCanceled } from "./webhookHandlers/handleSubScheduleCanceled.js"; -import { handleCusDiscountDeleted } from "./webhookHandlers/handleCusDiscountDeleted.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { createStripeCli } from "./utils.js"; -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"; +import { handleStripeWebhookEvent } from "./handleStripeWebhookEvent.js"; export const stripeWebhookRouter: Router = express.Router(); -const logStripeWebhook = ({ - req, - event, -}: { - req: ExtendedRequest; - event: Stripe.Event; -}) => { - req.logtail.info( - `${chalk.yellow("STRIPE").padEnd(18)} ${event.type.padEnd(30)} ${req.org.slug} | ${event.id}`, - ); -}; - stripeWebhookRouter.post( "/:orgId/:env", express.raw({ type: "application/json" }), async (request: any, response: any) => { const sig = request.headers["stripe-signature"]; - let event; + let event: Stripe.Event; const { orgId, env } = request.params; const { db } = request; @@ -103,13 +72,13 @@ stripeWebhookRouter.post( // event = request.body; - request.logtail = request.logtail.child({ + request.logger = request.logger.child({ context: { context: { // body: request.body, event_type: event.type, event_id: event.id, - // @ts-ignore + // @ts-expect-error object_id: `${event.data?.object?.id}` || "N/A", authType: AuthType.Stripe, org_id: orgId, @@ -119,229 +88,25 @@ stripeWebhookRouter.post( }, }); - let logger = request.logtail; - logStripeWebhook({ req: request, event }); + const logger = request.logger; try { - const stripeCli = createStripeCli({ org, env }); - switch (event.type) { - case "customer.subscription.created": - await handleSubCreated({ - db, - org, - subData: event.data.object, - env, - logger, - }); - break; - - case "customer.subscription.updated": - const subscription = event.data.object; - await handleSubscriptionUpdated({ - req: request, - db, - org, - subscription, - previousAttributes: event.data.previous_attributes, - env, - logger, - }); - break; - - case "customer.subscription.deleted": - await handleSubDeleted({ - req: request, - stripeCli, - data: event.data.object, - logger, - }); - break; - - case "checkout.session.completed": - const checkoutSession = event.data.object; - await handleCheckoutSessionCompleted({ - req: request, - db, - data: checkoutSession, - org, - env, - logger, - }); - break; - - // Triggered when payment through Stripe is successful - case "invoice.paid": - const invoice = event.data.object; - await handleInvoicePaid({ - db, - org, - invoiceData: invoice, - env, - event, - req: request, - }); - break; - - case "invoice.updated": - await handleInvoiceUpdated({ - stripeCli, - env, - event, - req: request, - }); - break; - - case "invoice.created": - const createdInvoice = event.data.object; - await handleInvoiceCreated({ - db, - org, - data: createdInvoice, - env, - logger, - }); - break; - - case "invoice.finalized": - const finalizedInvoice = event.data.object; - await handleInvoiceFinalized({ - db, - org, - data: finalizedInvoice, - env, - logger, - }); - break; - - case "subscription_schedule.canceled": - const canceledSchedule = event.data.object; - await handleSubscriptionScheduleCanceled({ - db, - org, - env, - schedule: canceledSchedule, - logger, - }); - break; - - case "customer.discount.deleted": - await handleCusDiscountDeleted({ - db, - org, - discount: event.data.object, - env, - logger, - res: response, - }); - break; - } + await handleStripeWebhookEvent({ + event, + db, + org, + env, + logger, + req: request, + }); + response.status(200).send(); } catch (error) { - if (error instanceof Stripe.errors.StripeError) { - if (error.message.includes("No such customer")) { - logger.warn(`stripe customer missing: ${error.message}`); - response.status(200).json({ message: "ok" }); - return; - } - - if (error.message.includes("Expired API Key provided")) { - // Disconnect Stripe - await unsetOrgStripeKeys({ - db, - org, - env, - }); - - response.status(200).json({ message: "ok" }); - return; - } - } - handleRequestError({ req: request, error, res: response, action: "stripe webhook", }); - return; } - - try { - await handleStripeWebhookRefresh({ - eventType: event.type, - data: event.data, - db, - org, - env, - logger, - }); - } catch (error) { - logger.error(`Stripe webhook, error refreshing cache!`, { error }); - } - - // DO NOT DELETE -- RESPONSIBLE FOR SENDING SUCCESSFUL RESPONSE TO STRIPE... - response.status(200).send(); }, ); - -const coreEvents = [ - "customer.subscription.created", - "customer.subscription.updated", - "customer.subscription.deleted", - "invoice.paid", - "invoice.created", - "invoice.finalized", - "subscription_schedule.canceled", - "checkout.session.completed", -]; - -export const handleStripeWebhookRefresh = async ({ - eventType, - data, - db, - org, - env, - logger, -}: { - eventType: string; - data: any; - db: DrizzleCli; - org: Organization; - env: AppEnv; - logger: any; -}) => { - if (coreEvents.includes(eventType)) { - let stripeCusId = data.object.customer; - if (!stripeCusId) { - logger.warn( - `stripe webhook cache refresh, object doesn't contain customer id`, - { - data: { - eventType, - object: data.object, - }, - }, - ); - return; - } - - let cus = await CusService.getByStripeId({ - db, - stripeId: stripeCusId, - }); - - if (!cus) { - logger.warn( - `Searched for customer by stripe id, but not found: ${stripeCusId}`, - ); - return; - } - - // logger.info(`Deleting cache for customer ${cus.id}`); - await deleteCusCache({ - db, - customerId: cus.id!, - org, - env, - }); - } -}; diff --git a/server/src/external/stripe/utils.ts b/server/src/external/stripe/utils.ts index d8c7c1c7e..a5a4ff0f9 100644 --- a/server/src/external/stripe/utils.ts +++ b/server/src/external/stripe/utils.ts @@ -1,47 +1,10 @@ import { - AppEnv, BillingInterval, - ErrCode, type Feature, Infinite, - type Organization, type UsagePriceConfig, } from "@autumn/shared"; -import Stripe from "stripe"; -import { decryptData } from "@/utils/encryptUtils.js"; -import RecaseError from "@/utils/errorUtils.js"; - -export const createStripeCli = ({ - org, - env, - // apiVersion, - legacyVersion, -}: { - org: Organization; - env: AppEnv; - // apiVersion?: string; - legacyVersion?: boolean; -}) => { - const encrypted = - env === AppEnv.Sandbox - ? org.stripe_config?.test_api_key - : org.stripe_config?.live_api_key; - - if (!encrypted) { - throw new RecaseError({ - message: `Please connect your Stripe ${env === AppEnv.Sandbox ? "test" : "live"} secret key. You can find it here: https://dashboard.stripe.com${env === AppEnv.Sandbox ? "/test" : ""}/apikeys`, - code: ErrCode.StripeConfigNotFound, - statusCode: 400, - }); - } - - const decrypted = decryptData(encrypted); - return new Stripe(decrypted, { - apiVersion: legacyVersion - ? ("2025-02-24.acacia" as any) - : "2025-07-30.basil", - }); -}; +import type Stripe from "stripe"; export const calculateMetered1Price = ({ product, diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts index dbae9addf..4428619cc 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted.ts @@ -7,6 +7,7 @@ import { } from "@autumn/shared"; import type { Stripe } from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.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"; @@ -16,10 +17,8 @@ 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 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"; diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts index c5884da13..c2aa69a8f 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleSetupCheckout.ts @@ -1,5 +1,6 @@ import { AttachBranch } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js"; import { handleOneOffFunction } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleOneOffFunction.js"; import { getDefaultAttachConfig } from "@/internal/customers/attach/attachUtils/getAttachConfig.js"; @@ -7,7 +8,6 @@ import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams import { isOneOff } from "@/internal/products/productUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; import { getCusPaymentMethod } from "../../stripeCusUtils.js"; -import { createStripeCli } from "../../utils.js"; export const handleSetupCheckout = async ({ req, diff --git a/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts b/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts index 2238f8c2f..c86c5f306 100644 --- a/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleCusDiscountDeleted.ts @@ -1,10 +1,10 @@ import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { CusService } from "@/internal/customers/CusService.js"; import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; import { RewardService } from "@/internal/rewards/RewardService.js"; import { notNullish } from "@/utils/genUtils.js"; -import { createStripeCli } from "../utils.js"; export async function handleCusDiscountDeleted({ db, diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts index 30ce229dd..eccbf7882 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleInvoiceCreated.ts @@ -10,6 +10,7 @@ import { } from "@autumn/shared"; import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { EntityService } from "@/internal/api/entities/EntityService.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; @@ -25,7 +26,6 @@ import { } from "../../stripeInvoiceUtils.js"; import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js"; import { getStripeSubs } from "../../stripeSubUtils.js"; -import { createStripeCli } from "../../utils.js"; import { handleContUsePrices } from "./handleContUsePrices.js"; import { handlePrepaidPrices } from "./handlePrepaidPrices.js"; import { handleUsagePrices } from "./handleUsagePrices.js"; diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts index c79c8d2f5..e2923ac9f 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceFinalized.ts @@ -1,23 +1,23 @@ import { - AppEnv, + type AppEnv, CusProductStatus, - FullCustomerPrice, - InvoiceStatus, - Organization, + type FullCustomerPrice, + type InvoiceStatus, + type Organization, } from "@autumn/shared"; -import Stripe from "stripe"; -import { createStripeCli } from "../utils.js"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; +import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; +import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js"; import { getFullStripeInvoice, getStripeExpandedInvoice, invoiceToSubId, updateInvoiceIfExists, } from "../stripeInvoiceUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; -import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js"; export const handleInvoiceFinalized = async ({ db, @@ -68,11 +68,11 @@ export const handleInvoiceFinalized = async ({ return; } - let prices = activeProducts.flatMap((cp) => + const prices = activeProducts.flatMap((cp) => cp.customer_prices.map((cpr: FullCustomerPrice) => cpr.price), ); - let invoiceItems = await getInvoiceItems({ + const invoiceItems = await getInvoiceItems({ stripeInvoice: invoice, prices: prices, logger, diff --git a/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts b/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts index 35298f9c7..078617a08 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoicePaid.ts @@ -7,6 +7,7 @@ import type { } from "@autumn/shared"; import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.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"; @@ -22,7 +23,6 @@ import { } from "../stripeInvoiceUtils.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 ({ @@ -153,7 +153,7 @@ export const handleInvoicePaid = async ({ env: AppEnv; event: Stripe.Event; }) => { - const logger = req.logtail; + const logger = req.logger; const stripeCli = createStripeCli({ org, env }); const invoice = await getFullStripeInvoice({ stripeCli, diff --git a/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts b/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts index 170615382..c61fb9d60 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoicePaidDiscount.ts @@ -10,6 +10,7 @@ import { Decimal } from "decimal.js"; import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { RewardService } from "@/internal/rewards/RewardService.js"; import { generateId } from "@/utils/genUtils.js"; import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js"; @@ -18,7 +19,6 @@ import { deleteCouponFromSub, } from "../stripeCouponUtils/deleteCouponFromCus.js"; import { invoiceToSubId } from "../stripeInvoiceUtils.js"; -import { createStripeCli } from "../utils.js"; export const handleInvoicePaidDiscount = async ({ db, diff --git a/server/src/external/stripe/webhookHandlers/handleSubCreated.ts b/server/src/external/stripe/webhookHandlers/handleSubCreated.ts index f9ab3b0e2..7f03f4f4f 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubCreated.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubCreated.ts @@ -1,27 +1,26 @@ -import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { + type AppEnv, BillingType, - CusProductStatus, - FullCusProduct, - FullCustomerPrice, - Organization, - Price, + type FullCusProduct, + type FullCustomerPrice, + type Organization, + type Price, } from "@autumn/shared"; -import { AppEnv } from "@autumn/shared"; -import Stripe from "stripe"; -import { createStripeCli } from "../utils.js"; -import { getStripeExpandedInvoice } from "../stripeInvoiceUtils.js"; -import { SubService } from "@/internal/subscriptions/SubService.js"; -import { generateId } from "@/utils/genUtils.js"; -import { getBillingType } from "@/internal/products/prices/priceUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { getFullStripeSub } from "../stripeSubUtils.js"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { InvoiceService } from "@/internal/invoices/InvoiceService.js"; import { getInvoiceItems } from "@/internal/invoices/invoiceUtils.js"; +import { getBillingType } from "@/internal/products/prices/priceUtils.js"; +import { SubService } from "@/internal/subscriptions/SubService.js"; +import { generateId } from "@/utils/genUtils.js"; +import { getStripeExpandedInvoice } from "../stripeInvoiceUtils.js"; import { getEarliestPeriodEnd, getEarliestPeriodStart, } from "../stripeSubUtils/convertSubUtils.js"; +import { getFullStripeSub } from "../stripeSubUtils.js"; export const handleSubCreated = async ({ db, @@ -56,7 +55,7 @@ export const handleSubCreated = async ({ } // Update autumn sub - let autumnSub = await SubService.getFromScheduleId({ + const autumnSub = await SubService.getFromScheduleId({ db, scheduleId: subscription.schedule as string, }); @@ -105,9 +104,9 @@ export const handleSubCreated = async ({ cusProds.length, ); - let batchUpdate = []; + const batchUpdate = []; for (const cusProd of cusProds) { - let subIds = cusProd.subscription_ids + const subIds = cusProd.subscription_ids ? [...cusProd.subscription_ids] : []; subIds.push(subscription.id); @@ -128,7 +127,7 @@ export const handleSubCreated = async ({ stripeInvoiceId: subscription.latest_invoice as string, }); - let invoiceItems = await getInvoiceItems({ + const invoiceItems = await getInvoiceItems({ stripeInvoice: invoice, prices: cusProd.customer_prices.map( (cpr: FullCustomerPrice) => cpr.price, @@ -155,19 +154,19 @@ export const handleSubCreated = async ({ } // Get cus prods for sub - let cusProds = await CusProductService.getByStripeSubId({ + const cusProds = await CusProductService.getByStripeSubId({ db, stripeSubId: subscription.id, orgId: org.id, env, }); - let handleInArrearWithEntity = async (cusProd: FullCusProduct) => { + const handleInArrearWithEntity = async (cusProd: FullCusProduct) => { if (!cusProd.internal_entity_id) { return; } - let arrearPrices = cusProd.customer_prices + const arrearPrices = cusProd.customer_prices .map((cp) => cp.price) .filter( (p: Price) => @@ -178,9 +177,9 @@ export const handleSubCreated = async ({ return; } - let itemsToDelete = []; + const itemsToDelete = []; for (const arrearPrice of arrearPrices) { - let subItem = subscription.items.data.find( + const subItem = subscription.items.data.find( (i) => i.price.id == arrearPrice.config?.stripe_price_id, ); @@ -211,7 +210,7 @@ export const handleSubCreated = async ({ } }; - let batchUpdate = []; + const batchUpdate = []; for (const cusProd of cusProds) { batchUpdate.push(handleInArrearWithEntity(cusProd)); } diff --git a/server/src/external/stripe/webhookHandlers/handleSubScheduleCanceled.ts b/server/src/external/stripe/webhookHandlers/handleSubScheduleCanceled.ts index 6454c5226..40a0d3d62 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubScheduleCanceled.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubScheduleCanceled.ts @@ -1,9 +1,7 @@ -import { AppEnv } from "@autumn/shared"; -import Stripe from "stripe"; -import { Organization } from "@autumn/shared"; -import { createStripeCli } from "../utils.js"; -import { SubService } from "@/internal/subscriptions/SubService.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import type { AppEnv, Organization } from "@autumn/shared"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; export const handleSubscriptionScheduleCanceled = async ({ diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts index 6893f4f99..adecff0f8 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated.ts @@ -5,15 +5,13 @@ import { type Organization, } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { SubService } from "@/internal/subscriptions/SubService.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; -import { createStripeCli } from "../utils.js"; import { handleSchedulePhaseCompleted } from "./handleSubUpdated/handleSchedulePhaseCompleted.js"; -import { - handleSubCanceled, - isSubCanceled, -} from "./handleSubUpdated/handleSubCanceled.js"; +import { handleSubCanceled } from "./handleSubUpdated/handleSubCanceled.js"; +import { handleSubPastDue } from "./handleSubUpdated/handleSubPastDue.js"; import { handleSubRenewed } from "./handleSubUpdated/handleSubRenewed.js"; export const handleSubscriptionUpdated = async ({ @@ -66,24 +64,12 @@ export const handleSubscriptionUpdated = async ({ past_due: CusProductStatus.PastDue, }; - // 1. Fetch subscription - const { canceled, canceledAt } = isSubCanceled({ - previousAttributes, - sub: fullSub, - }); - const updatedCusProducts = await CusProductService.updateByStripeSubId({ db, stripeSubId: subscription.id, updates: { status: subStatusMap[subscription.status] || CusProductStatus.Unknown, collection_method: fullSub.collection_method as CollectionMethod, - // canceled_at: canceled ? canceledAt : null, - // trial_ends_at: - // previousAttributes.status === "trialing" && - // subscription.status === "active" - // ? null - // : undefined, }, }); @@ -103,6 +89,14 @@ export const handleSubscriptionUpdated = async ({ org, }); + await handleSubPastDue({ + req, + previousAttributes, + sub: fullSub, + updatedCusProducts, + org, + }); + await handleSubRenewed({ req, prevAttributes: previousAttributes, diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts index 0d403ccef..7746250f6 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSchedulePhaseCompleted.ts @@ -4,6 +4,7 @@ import { cusProductToProduct, } from "@autumn/shared"; import type Stripe from "stripe"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { activateFutureProduct } from "@/internal/customers/cusProducts/cusProductUtils.js"; @@ -11,7 +12,6 @@ import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; import { notNullish } from "@/utils/genUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js"; -import { createStripeCli } from "../../utils.js"; export const handleSchedulePhaseCompleted = async ({ req, diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts index c9d37e2cd..32da293f4 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubCanceled.ts @@ -69,10 +69,16 @@ const updateCusProductCanceled = async ({ `Updating cus products for sub ${sub.id} to canceled | canceled_at: ${canceledAt}`, ); + const cancelsAt = sub.cancel_at ? sub.cancel_at * 1000 : undefined; + await CusProductService.updateByStripeSubId({ db, stripeSubId: sub.id, - updates: { canceled_at: canceledAt || Date.now(), canceled: true }, + updates: { + canceled_at: canceledAt || Date.now(), + canceled: true, + ended_at: cancelsAt, + }, }); }; @@ -102,7 +108,7 @@ export const handleSubCanceled = async ({ const canceledFromPortal = canceled && !isAutumnDowngrade; - const { db, env, logtail: logger } = req; + const { db, env, logger } = req; if (!canceledFromPortal || updatedCusProducts.length === 0) return; diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubPastDue.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubPastDue.ts new file mode 100644 index 000000000..839d30420 --- /dev/null +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubPastDue.ts @@ -0,0 +1,71 @@ +import { + AttachScenario, + type FullCusProduct, + type Organization, +} from "@autumn/shared"; +import type Stripe from "stripe"; +import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; + +export const isSubPastDue = ({ + previousAttributes, + sub, +}: { + previousAttributes: any; + sub: Stripe.Subscription; +}) => { + const wasPastDue = previousAttributes.status === "past_due"; + const isPastDue = sub.status === "past_due"; + + return { + pastDue: !wasPastDue && isPastDue, + }; +}; + +export const handleSubPastDue = async ({ + req, + previousAttributes, + org, + sub, + updatedCusProducts, +}: { + req: ExtendedRequest; + previousAttributes: any; + sub: Stripe.Subscription; + org: Organization; + updatedCusProducts: FullCusProduct[]; +}) => { + const { pastDue } = isSubPastDue({ + previousAttributes, + sub, + }); + + const { env, logger } = req; + + if (!pastDue || updatedCusProducts.length === 0) return; + + logger.info( + `Subscription ${sub.id} is now past due, firing webhooks for ${updatedCusProducts.length} customer product(s)`, + ); + + if (!org.config.sync_status) return; + + for (const cusProd of updatedCusProducts) { + try { + await addProductsUpdatedWebhookTask({ + req, + internalCustomerId: cusProd.internal_customer_id, + org, + env, + customerId: null, + logger, + scenario: AttachScenario.PastDue, + cusProduct: cusProd, + }); + } catch (error) { + logger.error("Failed to add products updated webhook task to queue", { + error, + }); + } + } +}; diff --git a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts index d4dcfcd2e..7268116da 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubUpdated/handleSubRenewed.ts @@ -1,13 +1,14 @@ +import { AttachScenario, type FullCusProduct } from "@autumn/shared"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; +import { isMultiProductSub } from "@/internal/customers/attach/mergeUtils/mergeUtils.js"; +import { getSubScenarioFromCache } from "@/internal/customers/cusCache/subCacheUtils.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js"; import { notNullish, nullish } from "@/utils/genUtils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { AttachScenario, FullCusProduct } from "@autumn/shared"; -import Stripe from "stripe"; -import { isMultiProductSub } from "@/internal/customers/attach/mergeUtils/mergeUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { getSubScenarioFromCache } from "@/internal/customers/cusCache/subCacheUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; + const isSubRenewed = ({ previousAttributes, sub, @@ -62,21 +63,21 @@ export const handleSubRenewed = async ({ sub: Stripe.Subscription; updatedCusProducts: FullCusProduct[]; }) => { - const { db, org, env, logtail: logger } = req; + const { db, org, env, logger } = req; const { renewed } = isSubRenewed({ previousAttributes: prevAttributes, sub, }); - if (!renewed || updatedCusProducts.length == 0) return; + if (!renewed || updatedCusProducts.length === 0) return; const subScenario = await getSubScenarioFromCache({ subId: sub.id }); console.log(`Renewed: ${renewed}, subScenario: ${subScenario}`); if (subScenario === AttachScenario.Renew) return; const customer = updatedCusProducts[0].customer; - let cusProducts = await CusProductService.list({ + const cusProducts = await CusProductService.list({ db, internalCustomerId: customer!.internal_id, }); @@ -88,18 +89,18 @@ export const handleSubRenewed = async ({ await CusProductService.updateByStripeSubId({ db, stripeSubId: sub.id, - updates: { canceled_at: null, canceled: false }, + updates: { canceled_at: null, canceled: false, ended_at: null }, }); if (!org.config.sync_status) return; - let { curScheduledProduct } = getExistingCusProducts({ + const { curScheduledProduct } = getExistingCusProducts({ product: updatedCusProducts[0].product, cusProducts, internalEntityId: updatedCusProducts[0].internal_entity_id, }); - let deletedCusProducts: FullCusProduct[] = []; + const deletedCusProducts: FullCusProduct[] = []; if (curScheduledProduct) { logger.info( @@ -115,7 +116,7 @@ export const handleSubRenewed = async ({ } try { - for (let cusProd of updatedCusProducts) { + for (const cusProd of updatedCusProducts) { await addProductsUpdatedWebhookTask({ req, internalCustomerId: cusProd.internal_customer_id, diff --git a/server/src/external/svix/svixUtils.ts b/server/src/external/svix/svixUtils.ts index 942697f46..9608fd8a0 100644 --- a/server/src/external/svix/svixUtils.ts +++ b/server/src/external/svix/svixUtils.ts @@ -1,5 +1,4 @@ -import { AppEnv } from "@autumn/shared"; -import { Organization } from "@autumn/shared"; +import { AppEnv, type Organization } from "@autumn/shared"; import { Svix } from "svix"; import { logger } from "../logtail/logtailUtils.js"; @@ -35,7 +34,7 @@ export const getSvixAppId = ({ env: AppEnv; }) => { const svixConfig = org.svix_config; - return env == AppEnv.Live + return env === AppEnv.Live ? svixConfig?.live_app_id : svixConfig?.sandbox_app_id; }; diff --git a/server/src/external/webhooks/connectWebhookRouter.ts b/server/src/external/webhooks/connectWebhookRouter.ts new file mode 100644 index 000000000..755d1315b --- /dev/null +++ b/server/src/external/webhooks/connectWebhookRouter.ts @@ -0,0 +1,121 @@ +import { + type AppEnv, + AuthType, + type Feature, + type Organization, +} from "@autumn/shared"; +import express, { type Router } from "express"; +import type { Context } from "hono"; +import type { Stripe } from "stripe"; +import { + getStripeWebhookSecret, + initMasterStripe, +} from "@/external/connect/initStripeCli.js"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { handleStripeWebhookEvent } from "../stripe/handleStripeWebhookEvent.js"; + +export const connectWebhookRouter: Router = express.Router(); + +export const handleConnectWebhook = async (c: Context) => { + const ctx = c.get("ctx"); + const { db, logger } = ctx; + const { env } = c.req.param() as { env: AppEnv }; + + // Initial logging of event body... + const body = await c.req.json(); + logger.info(`connect webhook received (${env})`, { + body, + }); + + let masterStripe: Stripe; + try { + masterStripe = initMasterStripe(); + } catch (error) { + logger.error(`Failed to initialize master stripe client ${error}`); + return c.json(200); + } + let event: Stripe.Event; + + // Step 1: Get webhook secret + const webhookSecret = await getStripeWebhookSecret({ + db, + orgId: c.req.query("org_id"), + env, + }); + + // Step 2: Verify webhook event + try { + const rawBody = await c.req.text(); + const signature = c.req.header("stripe-signature") || ""; + + event = await masterStripe.webhooks.constructEventAsync( + rawBody, + signature, + webhookSecret, + ); + } catch (err: any) { + logger.error(`Webhook verification error: ${err.message}`); + return c.json({ error: err.message }, 400); + } + + // Step 3: Get org and features + const accountId = event.account; + if (!accountId) { + logger.error(`Account ID not found in webhook event`); + return c.json({ error: "Account ID not found" }, 200); + } + + let org: Organization; + let features: Feature[]; + try { + const data = await OrgService.getByAccountId({ + db, + accountId, + }); + org = data.org; + features = data.features; + } catch { + logger.error( + `Account ID ${accountId} not linked to any org, skipping Stripe webhook`, + ); + return c.json( + { message: "Account ID not linked to any org, skipping Stripe webhook" }, + 200, + ); + } + + ctx.org = org; + ctx.features = features; + ctx.env = env as AppEnv; + ctx.logger = ctx.logger.child({ + context: { + context: { + event_type: event.type, + event_id: event.id, + // @ts-expect-error + object_id: `${event.data?.object?.id}` || "N/A", + authType: AuthType.Stripe, + org_id: org.id, + org_slug: org.slug, + env, + }, + }, + }); + + try { + await handleStripeWebhookEvent({ + event, + db, + org, + env: env as AppEnv, + logger, + req: ctx as ExtendedRequest, + }); + return c.json({ message: "Webhook received" }, 200); + } catch (error) { + logger.error(`Stripe webhook, error: ${error}`, { error }); + return c.json({ message: "Webhook received, internal server error" }, 200); + } +}; diff --git a/server/src/external/webhooks/webhooksRouter.ts b/server/src/external/webhooks/webhooksRouter.ts index b9e7ad3e5..c8a957962 100644 --- a/server/src/external/webhooks/webhooksRouter.ts +++ b/server/src/external/webhooks/webhooksRouter.ts @@ -1,7 +1,6 @@ -import express, { Router } from "express"; - -import { stripeWebhookRouter } from "../stripe/stripeWebhooks.js"; +import express, { type Router } from "express"; import { autumnWebhookRouter } from "../autumn/autumnWebhookRouter.js"; +import { stripeWebhookRouter } from "../stripe/stripeWebhooks.js"; const webhooksRouter: Router = express.Router(); @@ -9,4 +8,6 @@ webhooksRouter.use("/stripe", stripeWebhookRouter); webhooksRouter.use("/autumn", autumnWebhookRouter); +// webhooksRouter.use("/connect", connectWebhookRouter); + export default webhooksRouter; diff --git a/server/src/honoMiddlewares/errorMiddleware.ts b/server/src/honoMiddlewares/errorMiddleware.ts index c5770c3b1..c7d475647 100644 --- a/server/src/honoMiddlewares/errorMiddleware.ts +++ b/server/src/honoMiddlewares/errorMiddleware.ts @@ -5,126 +5,7 @@ import Stripe from "stripe"; import { ZodError } from "zod/v4"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; import RecaseError, { formatZodError } from "@/utils/errorUtils.js"; -import { matchRoute } from "./middlewareUtils.js"; - -/** - * Handle special error cases that should use warn instead of error logging - * Returns a response if the error matches a special case, null otherwise - */ -const handleSpecialErrorCases = ( - err: Error, - c: Context, - ctx: any, - logger: any, -) => { - const url = c.req.url; - - // Special case 1: EntityNotFound - use warn instead of error - if (err instanceof RecaseError && err.code === ErrCode.EntityNotFound) { - logger.warn(`${err.message}, org: ${ctx.org?.slug || "unknown"}`); - return c.json( - { - message: err.message, - code: err.code, - env: ctx.env, - }, - 404, - ); - } - - // Special case 2: Stripe exchange router invalid API key - if ( - err instanceof Stripe.errors.StripeError && - url.includes("/exchange") && - err.message.includes("Invalid API Key provided") - ) { - logger.warn("Exchange router, invalid API Key provided"); - return c.json( - { - message: err.message, - code: ErrCode.InvalidRequest, - env: ctx.env, - }, - 400, - ); - } - - // Special case 3: Billing portal config error - if ( - err instanceof Stripe.errors.StripeError && - url.includes("/billing_portal") && - err.message.includes("Provide a configuration or create your default") - ) { - logger.warn(`Billing portal config error, org: ${ctx.org?.slug}`); - return c.json( - { - message: err.message, - code: ErrCode.InvalidRequest, - env: ctx.env, - }, - 404, - ); - } - - // Special case 4: Billing portal return_url error - if ( - err instanceof Stripe.errors.StripeError && - url.includes("/billing_portal") && - err.message.includes("Invalid URL: An explicit scheme (such as https)") - ) { - logger.warn(`Billing portal return_url error, org: ${ctx.org?.slug}`); - return c.json( - { - message: err.message, - code: ErrCode.InvalidRequest, - env: ctx.env, - }, - 400, - ); - } - - // Special case 5: Zod error on /attach - convert to RecaseError - if (err instanceof ZodError && url.includes("/attach")) { - const formattedError = formatZodError(err); - logger.warn( - `ATTACH ZOD ERROR (${ctx.org?.slug || "unknown"}): ${formattedError}`, - ); - - return c.json( - { - message: formattedError, - code: ErrCode.InvalidInputs, - env: ctx.env, - }, - 400, - ); - } - - // Special case 6: CustomerNotFound on customer routes - const pathname = new URL(url).pathname; - if ( - err instanceof RecaseError && - err.code === ErrCode.CustomerNotFound && - matchRoute({ - url: pathname, - method: c.req.method, - pattern: { url: "/customers/:customer_id", method: "GET" }, - }) - ) { - logger.warn(`${err.message}, org: ${ctx.org?.slug || "unknown"}`); - return c.json( - { - message: err.message, - code: err.code, - env: ctx.env, - }, - 404, - ); - } - - // No special case matched - return null; -}; +import { handleErrorSkip } from "./errorSkipMiddleware.js"; /** * Hono error handler middleware @@ -146,9 +27,9 @@ export const errorMiddleware = (err: Error, c: Context) => { ); } - // Check for special error cases first - const specialCaseResponse = handleSpecialErrorCases(err, c, ctx, logger); - if (specialCaseResponse) return specialCaseResponse; + // Check for error skip cases first (warn-level errors) + const skipResponse = handleErrorSkip(err, c); + if (skipResponse) return skipResponse; // 1. Handle RecaseError (our custom errors) if (err instanceof RecaseError || err instanceof SharedRecaseError) { diff --git a/server/src/honoMiddlewares/errorSkipMiddleware.ts b/server/src/honoMiddlewares/errorSkipMiddleware.ts new file mode 100644 index 000000000..84619431b --- /dev/null +++ b/server/src/honoMiddlewares/errorSkipMiddleware.ts @@ -0,0 +1,231 @@ +import { + CusErrorCode, + ErrCode, + ProductErrorCode, + RecaseError as SharedRecaseError, +} from "@autumn/shared"; +import type { Context } from "hono"; +import type { ContentfulStatusCode } from "hono/utils/http-status"; +import Stripe from "stripe"; +import { ZodError } from "zod/v4"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; +import RecaseError, { formatZodError } from "@/utils/errorUtils.js"; +import { matchRoute } from "./middlewareUtils.js"; + +// ============================================================================ +// ERROR SKIP CONFIGURATION +// ============================================================================ + +/** + * Simple route-based error code skipping + * Add routes here to skip specific error codes (logged as warnings, returns appropriate status) + */ +const ROUTE_ERROR_SKIP_MAP = [ + // { + // route: "/products/:productId/count", + // method: "GET", + // skipErrorCodes: [ProductErrorCode.ProductNotFound], + // }, + { + route: "/customers/:customer_id/aksdjnalksjnd", + method: "GET", + skipErrorCodes: [ErrCode.CustomerNotFound], + }, +] as const; + +/** Global error codes that should be logged as warnings instead of errors (all routes) */ +const GLOBAL_WARN_ERROR_CODES: string[] = [ + ProductErrorCode.ProductNotFound, + CusErrorCode.CustomerNotFound, + ErrCode.CustomerNotFound, + ErrCode.EntityNotFound, +]; + +/** Advanced route-specific error handling rules (for complex matching logic) */ +const ROUTE_SPECIFIC_RULES: Array<{ + name: string; + match: (err: Error, c: Context) => boolean; + statusCode: ContentfulStatusCode; +}> = []; + +/** Stripe-specific error handling rules */ +const STRIPE_RULES = [ + { + name: "Exchange router invalid API key", + match: (err: Error, c: Context) => + err instanceof Stripe.errors.StripeError && + c.req.url.includes("/exchange") && + err.message.includes("Invalid API Key provided"), + statusCode: 400, + code: ErrCode.InvalidRequest, + }, + { + name: "Billing portal config error", + match: (err: Error, c: Context) => + err instanceof Stripe.errors.StripeError && + c.req.url.includes("/billing_portal") && + err.message.includes("Provide a configuration or create your default"), + statusCode: 404, + code: ErrCode.InvalidRequest, + }, + { + name: "Billing portal return_url error", + match: (err: Error, c: Context) => + err instanceof Stripe.errors.StripeError && + c.req.url.includes("/billing_portal") && + err.message.includes("Invalid URL: An explicit scheme (such as https)"), + statusCode: 400, + code: ErrCode.InvalidRequest, + }, +] as const; + +/** Zod-specific error handling rules */ +const ZOD_RULES = [ + { + name: "Zod error on /attach", + match: (err: Error, c: Context) => + err instanceof ZodError && c.req.url.includes("/attach"), + statusCode: 400, + format: (err: ZodError) => formatZodError(err), + }, +] as const; + +const createErrorResponse = ({ + c, + ctx, + message, + code, + statusCode, +}: { + c: Context; + ctx: any; + message: string; + code: string; + statusCode: ContentfulStatusCode; +}) => { + return c.json( + { + message, + code, + env: ctx.env, + }, + statusCode, + ); +}; + +/** + * Handles special error cases that should use warn logging instead of error logging. + * Returns a response if handled, null otherwise to continue to main error handler. + */ +export const handleErrorSkip = (err: Error, c: Context) => { + const ctx = c.get("ctx"); + const logger = ctx?.logger; + + if (!logger) { + return null; // Let main error handler deal with this + } + + // 1. Check route-based error code skipping (simplest case) + if (err instanceof RecaseError || err instanceof SharedRecaseError) { + const pathname = new URL(c.req.url).pathname; + + for (const skipRule of ROUTE_ERROR_SKIP_MAP) { + if ( + skipRule.skipErrorCodes.includes(err.code as any) && + matchRoute({ + url: pathname, + method: c.req.method, + pattern: { url: skipRule.route, method: skipRule.method }, + }) + ) { + logger.warn( + `${err.message}, org: ${ctx.org?.slug || "unknown"} [route skip: ${skipRule.route}]`, + ); + return createErrorResponse({ + c, + ctx, + message: err.message, + code: err.code, + statusCode: 404, + }); + } + } + } + + // 2. Check global warn-level error codes + if ( + (err instanceof RecaseError || err instanceof SharedRecaseError) && + GLOBAL_WARN_ERROR_CODES.includes(err.code) + ) { + logger.warn( + `${err.message}, org: ${ctx.org?.slug || "unknown"}, path: ${c.req.path}`, + ); + return createErrorResponse({ + c, + ctx, + message: err.message, + code: err.code, + statusCode: 404, + }); + } + + // 3. Check advanced route-specific rules + for (const rule of ROUTE_SPECIFIC_RULES) { + if (rule.match(err, c)) { + const recaseErr = err as RecaseError; + logger.warn(`${recaseErr.message}, org: ${ctx.org?.slug || "unknown"}`); + return createErrorResponse({ + c, + ctx, + message: recaseErr.message, + code: recaseErr.code, + statusCode: rule.statusCode, + }); + } + } + + // 4. Check Stripe-specific rules + for (const rule of STRIPE_RULES) { + if (rule.match(err, c)) { + const stripeErr = err as Stripe.errors.StripeError; + logger.warn(`${rule.name}, org: ${ctx.org?.slug || "unknown"}`); + return createErrorResponse({ + c, + ctx, + message: stripeErr.message, + code: rule.code, + statusCode: rule.statusCode, + }); + } + } + + // 5. Check Zod-specific rules + for (const rule of ZOD_RULES) { + if (rule.match(err, c)) { + const zodErr = err as ZodError; + const formattedError = rule.format(zodErr); + logger.warn( + `ZOD ERROR (${ctx.org?.slug || "unknown"}): ${formattedError}`, + ); + return createErrorResponse({ + c, + ctx, + message: formattedError, + code: ErrCode.InvalidInputs, + statusCode: rule.statusCode, + }); + } + } + + // No special case matched - continue to main error handler + return null; +}; + +/** + * Middleware wrapper for error skip handling + * Note: This doesn't actually prevent errors from reaching onError handler + * It's used within the main error handler to check for skip cases + */ +export const errorSkipMiddleware = (err: Error, c: Context) => { + return handleErrorSkip(err, c); +}; diff --git a/server/src/honoMiddlewares/routeHandler.ts b/server/src/honoMiddlewares/routeHandler.ts index edc59b053..377505cca 100644 --- a/server/src/honoMiddlewares/routeHandler.ts +++ b/server/src/honoMiddlewares/routeHandler.ts @@ -14,6 +14,7 @@ type ValidatedContext< E extends Env, Body extends ZodType | undefined = undefined, Query extends ZodType | undefined = undefined, + Params extends ZodType | undefined = undefined, > = Context< E, any, @@ -21,10 +22,12 @@ type ValidatedContext< in: { json: Body extends ZodType ? z.infer : unknown; query: Query extends ZodType ? z.infer : unknown; + param: Params extends ZodType ? z.infer : unknown; }; out: { json: Body extends ZodType ? z.infer : unknown; query: Query extends ZodType ? z.infer : unknown; + param: Params extends ZodType ? z.infer : unknown; }; } >; @@ -41,14 +44,18 @@ type VersionedSchemas = Partial< /** * Create a type-safe route with validation that preserves full type inference! * - * Supports two patterns: + * Supports validation for body, query, and params: * * **Pattern 1: Single version (most endpoints)** * ```ts * export const createProduct = createRoute({ * body: CreateProductSchema, + * query: ProductQuerySchema, + * params: ProductParamsSchema, * handler: async (c) => { - * const body = c.req.valid("json"); // ✅ Fully typed! + * const body = c.req.valid("json"); // ✅ Fully typed! + * const query = c.req.valid("query"); // ✅ Fully typed! + * const params = c.req.valid("param"); // ✅ Fully typed! * return c.json({ success: true }); * } * }); @@ -74,15 +81,17 @@ type VersionedSchemas = Partial< export function createRoute< Body extends ZodType | undefined = undefined, Query extends ZodType | undefined = undefined, + Params extends ZodType | undefined = undefined, >(opts: { body?: Body; versionedBody?: Body extends ZodType ? VersionedSchemas : never; query?: Query; versionedQuery?: Query extends ZodType ? VersionedSchemas : never; + params?: Params; resource?: AffectedResource; withTx?: boolean; handler: ( - c: ValidatedContext, + c: ValidatedContext, ) => Response | Promise; }) { const middlewares: MiddlewareHandler[] = []; @@ -114,7 +123,14 @@ export function createRoute< middlewares.push(validator("query", opts.query)); } - const wrappedHandler = async (c: ValidatedContext) => { + // Params validator (no versioned variant) + if (opts.params) { + middlewares.push(validator("param", opts.params)); + } + + const wrappedHandler = async ( + c: ValidatedContext, + ) => { c.set("validated", true); if (opts.withTx) { diff --git a/server/src/index.ts b/server/src/index.ts index e21f7dd41..1576806ca 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -29,7 +29,6 @@ import { client, db } from "./db/initDrizzle.js"; import { CacheManager } from "./external/caching/CacheManager.js"; import { ClickHouseManager } from "./external/clickhouse/ClickHouseManager.js"; import { logger } from "./external/logtail/logtailUtils.js"; -import { createPosthogCli } from "./external/posthog/createPosthogCli.js"; import webhooksRouter from "./external/webhooks/webhooksRouter.js"; import { redirectToHono } from "./initHono.js"; import { apiRouter } from "./internal/api/apiRouter.js"; @@ -38,7 +37,6 @@ import { QueueManager } from "./queue/QueueManager.js"; import { auth } from "./utils/auth.js"; import { generateId } from "./utils/genUtils.js"; import { checkEnvVars } from "./utils/initUtils.js"; - const tracer = trace.getTracer("express"); checkEnvVars(); @@ -128,8 +126,6 @@ const init = async () => { app.all("/api/auth/*", toNodeHandler(auth)); - const posthog = createPosthogCli(); - // Initialize managers in parallel for faster startup await Promise.all([ QueueManager.getInstance(), @@ -141,7 +137,6 @@ const init = async () => { req.env = req.env = req.headers.app_env || AppEnv.Sandbox; req.db = db; req.clickhouseClient = await ClickHouseManager.getClient(); - req.posthog = posthog; req.id = req.headers["rndr-id"] || generateId("local_req"); req.timestamp = Date.now(); @@ -165,12 +160,11 @@ const init = async () => { // Store span on request for potential use in other middleware/handlers req.span = span; - req.logtail = logger.child({ + req.logger = logger.child({ context: { req: reqContext, }, }); - req.logger = req.logtail; const endSpan = () => { try { @@ -203,7 +197,7 @@ const init = async () => { app.use(express.json()); app.use(async (req: any, res: any, next: any) => { - req.logtail.info(`${req.method} ${req.originalUrl}`, { + req.logger.info(`${req.method} ${req.originalUrl}`, { context: { body: req.body, }, diff --git a/server/src/initHono.ts b/server/src/initHono.ts index 7653b1c20..330336f4c 100644 --- a/server/src/initHono.ts +++ b/server/src/initHono.ts @@ -1,9 +1,11 @@ import { getRequestListener } from "@hono/node-server"; import { Hono } from "hono"; import { cors } from "hono/cors"; +import { handleConnectWebhook } from "./external/webhooks/connectWebhookRouter.js"; import { analyticsMiddleware } from "./honoMiddlewares/analyticsMiddleware.js"; import { apiVersionMiddleware } from "./honoMiddlewares/apiVersionMiddleware.js"; import { baseMiddleware } from "./honoMiddlewares/baseMiddleware.js"; +import { betterAuthMiddleware } from "./honoMiddlewares/betterAuthMiddleware.js"; import { errorMiddleware } from "./honoMiddlewares/errorMiddleware.js"; import { orgConfigMiddleware } from "./honoMiddlewares/orgConfigMiddleware.js"; import { queryMiddleware } from "./honoMiddlewares/queryMiddleware.js"; @@ -12,7 +14,12 @@ import { secretKeyMiddleware } from "./honoMiddlewares/secretKeyMiddleware.js"; import { traceMiddleware } from "./honoMiddlewares/traceMiddleware.js"; import type { HonoEnv } from "./honoUtils/HonoEnv.js"; import { cusRouter } from "./internal/customers/cusRouter.js"; +import { internalCusRouter } from "./internal/customers/internalCusRouter.js"; +import { handleOAuthCallback } from "./internal/orgs/handlers/stripeHandlers/handleOAuthCallback.js"; +import { honoOrgRouter } from "./internal/orgs/orgRouter.js"; import { honoPlatformRouter } from "./internal/platform/honoPlatformRouter.js"; +import { platformBetaRouter } from "./internal/platform/platformBeta/platformBetaRouter.js"; +import { internalProductRouter } from "./internal/products/internalProductRouter.js"; import { honoProductRouter } from "./internal/products/productRouter.js"; import { auth } from "./utils/auth.js"; @@ -66,35 +73,37 @@ export const createHonoApp = () => { return auth.handler(c.req.raw); }); - // Step 1: Base middleware - sets up ctx (db, logger, etc.) - only for v1 routes - app.use("/v1/*", baseMiddleware); + // OAuth callback (needs to be before middleware) + app.get("/stripe/oauth_callback", handleOAuthCallback); - // Step 2: Tracing middleware - handles OpenTelemetry spans - only for v1 routes - app.use("/v1/*", traceMiddleware); + // Step 1: Base middleware - sets up ctx (db, logger, etc.) + app.use("*", baseMiddleware); + app.use("*", traceMiddleware); - // Step 3: Auth middleware - verifies secret key and populates auth context + // Webhook routes + app.post("/webhooks/connect/:env", handleConnectWebhook); + + // API Middleware app.use("/v1/*", secretKeyMiddleware); - - // Step 4: Org config middleware - allows config overrides via header app.use("/v1/*", orgConfigMiddleware); - - // Step 5: API Version middleware - validates x-api-version header app.use("/v1/*", apiVersionMiddleware); - - // Step 6: Refresh cache middleware - clears customer cache after successful mutations app.use("/v1/*", refreshCacheMiddleware); - - // Step 7: Analytics middleware - enriches logger context and logs responses app.use("/v1/*", analyticsMiddleware); - - // Step 8: Query middleware - handles query parsing and validation app.use("/v1/*", queryMiddleware()); + // API Routes app.route("v1/customers", cusRouter); app.route("v1/products", honoProductRouter); app.route("v1/platform", honoPlatformRouter); + app.route("v1/platform/beta", platformBetaRouter); + app.route("v1/organization", honoOrgRouter); + + // Internal/dashboard routes - use betterAuthMiddleware for session auth + app.use("/products/*", betterAuthMiddleware); + app.route("/products", internalProductRouter); + app.use("/customers/*", betterAuthMiddleware); + app.route("/customers", internalCusRouter); - // Error handler - must be defined after all routes and middleware app.onError(errorMiddleware); // Create request listener for integration with Express diff --git a/server/src/internal/admin/withAdminAuth.ts b/server/src/internal/admin/withAdminAuth.ts index 6f99ac786..2e3f7b305 100644 --- a/server/src/internal/admin/withAdminAuth.ts +++ b/server/src/internal/admin/withAdminAuth.ts @@ -5,7 +5,7 @@ import { ADMIN_USER_IDs } from "@/utils/constants.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; export const withAdminAuth = async (req: any, res: any, next: NextFunction) => { - const { logtail: logger, userId } = req as ExtendedRequest; + const { logger } = req as ExtendedRequest; try { const data = await auth.api.getSession({ diff --git a/server/src/internal/api/apiRouter.ts b/server/src/internal/api/apiRouter.ts index 5b946b0c8..c0b60695d 100644 --- a/server/src/internal/api/apiRouter.ts +++ b/server/src/internal/api/apiRouter.ts @@ -13,10 +13,8 @@ import { handleCreateBillingPortal } from "../customers/handlers/handleCreateBil import { featureRouter } from "../features/featureRouter.js"; import { internalFeatureRouter } from "../features/internalFeatureRouter.js"; import { migrationRouter } from "../migrations/migrationRouter.js"; -import { handleConnectStripe } from "../orgs/handlers/handleConnectStripe.js"; -import { handleDeleteStripe } from "../orgs/handlers/handleDeleteStripe.js"; import { handleGetOrg } from "../orgs/handlers/handleGetOrg.js"; -import { platformRouter } from "../platform/platformRouter.js"; +import { platformRouter } from "../platform/platformLegacy/platformRouter.js"; import { productBetaRouter, productRouter } from "../products/productRouter.js"; import { componentRouter } from "./components/componentRouter.js"; import { entityRouter } from "./entities/entityRouter.js"; @@ -68,9 +66,9 @@ apiRouter.post("/billing_portal", handleCreateBillingPortal); apiRouter.use("/query", analyticsRouter); apiRouter.use("/platform", platformRouter); -// Used for tests... -apiRouter.post("/organization/stripe", handleConnectStripe); -apiRouter.delete("/organization/stripe", handleDeleteStripe); +// // Used for tests... +// apiRouter.post("/organization/stripe", ...handleConnectStripe); +// apiRouter.delete("/organization/stripe", ...handleDeleteStripe); apiRouter.get("/organization", handleGetOrg); export { apiRouter }; diff --git a/server/src/internal/api/batch/handlers/handleBatchCustomers.ts b/server/src/internal/api/batch/handlers/handleBatchCustomers.ts index 48cd0ae6e..92e477c86 100644 --- a/server/src/internal/api/batch/handlers/handleBatchCustomers.ts +++ b/server/src/internal/api/batch/handlers/handleBatchCustomers.ts @@ -72,7 +72,7 @@ export const handleBatchCustomers = async (req: any, res: any) => offset: query.offset, features: req.features, statuses: query.statuses ?? [], - logger: req.logtail, + logger: req.logger, apiVersion: req.apiVersion, }); }, diff --git a/server/src/internal/api/entities/entityUtils.ts b/server/src/internal/api/entities/entityUtils.ts index 72757a54e..b1f247493 100644 --- a/server/src/internal/api/entities/entityUtils.ts +++ b/server/src/internal/api/entities/entityUtils.ts @@ -1,6 +1,21 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { + type AppEnv, + BillingType, + type Customer, + type Entitlement, + type Entity, + EntityExpand, + ErrCode, + type Feature, + type FullCustomerEntitlement, + type FullCustomerPrice, + type Organization, + type UsagePriceConfig, +} from "@autumn/shared"; +import { StatusCodes } from "http-status-codes"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { submitUsageToStripe } from "@/external/stripe/stripeMeterUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; import { getBillingType, @@ -8,21 +23,6 @@ import { } from "@/internal/products/prices/priceUtils.js"; import RecaseError from "@/utils/errorUtils.js"; import { notNullish } from "@/utils/genUtils.js"; -import { - AppEnv, - BillingType, - Customer, - Entitlement, - Entity, - EntityExpand, - ErrCode, - Feature, - FullCustomerEntitlement, - FullCustomerPrice, - Organization, - UsagePriceConfig, -} from "@autumn/shared"; -import { StatusCodes } from "http-status-codes"; export const getLinkedCusEnt = ({ linkedFeature, @@ -32,7 +32,7 @@ export const getLinkedCusEnt = ({ cusEnts: any; }) => { // Get linked cus ent... - let linkedCusEnt = cusEnts.find( + const linkedCusEnt = cusEnts.find( (e: any) => e.entitlement.feature.id === linkedFeature.id, ); @@ -48,7 +48,7 @@ export const entityFeatureIdExists = ({ }: { cusEnt: FullCustomerEntitlement; }) => { - let ent = cusEnt.entitlement; + const ent = cusEnt.entitlement; return notNullish(ent.entity_feature_id); }; @@ -102,7 +102,7 @@ export const removeEntityFromCusEnt = async ({ env: AppEnv; }) => { // isLinked - let isLinked = isLinkedToEntity({ + const isLinked = isLinkedToEntity({ cusEnt, entity, }); @@ -111,22 +111,22 @@ export const removeEntityFromCusEnt = async ({ return; } - let entitlement = cusEnt.entitlement; + const entitlement = cusEnt.entitlement; console.log( `Linked cus ent: ${entitlement.feature.id}, isLinked: ${isLinked}`, ); // Delete cus ent ids - let newEntities = structuredClone(cusEnt.entities!); + const newEntities = structuredClone(cusEnt.entities!); // TODO: Send usage to stripe if cus price exists - let stripeCli = createStripeCli({ + const stripeCli = createStripeCli({ org, env, }); if (cusPrice) { - let config = cusPrice.price.config as UsagePriceConfig; - let billingType = getBillingType(config); + const config = cusPrice.price.config as UsagePriceConfig; + const billingType = getBillingType(config); if (billingType == BillingType.UsageInArrear) { let usage = -newEntities[entity.id]?.balance; @@ -163,8 +163,8 @@ export const removeEntityFromCusEnt = async ({ export const parseEntityExpand = (expand: string): EntityExpand[] => { if (expand) { - let options = expand.split(","); - let result: EntityExpand[] = []; + const options = expand.split(","); + const result: EntityExpand[] = []; for (const option of options) { if (!Object.values(EntityExpand).includes(option as EntityExpand)) { throw new RecaseError({ diff --git a/server/src/internal/api/entities/handlers/handleDeleteEntity.ts b/server/src/internal/api/entities/handlers/handleDeleteEntity.ts index c77d31b1a..71c90f830 100644 --- a/server/src/internal/api/entities/handlers/handleDeleteEntity.ts +++ b/server/src/internal/api/entities/handlers/handleDeleteEntity.ts @@ -1,25 +1,25 @@ -import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; -import { EntityService } from "../EntityService.js"; -import { StatusCodes } from "http-status-codes"; import { CusProductStatus, ErrCode } from "@autumn/shared"; -import { CusService } from "@/internal/customers/CusService.js"; -import { adjustAllowance } from "@/trigger/adjustAllowance.js"; +import { StatusCodes } from "http-status-codes"; import { handleCustomerRaceCondition } from "@/external/redis/redisUtils.js"; +import { CusService } from "@/internal/customers/CusService.js"; import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; import { findLinkedCusEnts, findMainCusEntForFeature, } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js"; -import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js"; import { deleteEntityFromCusEnt, replaceEntityInCusEnt, } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/linkedCusEntUtils.js"; +import { RepService } from "@/internal/customers/cusProducts/cusEnts/RepService.js"; import { cancelSubsForEntity } from "@/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.js"; +import { adjustAllowance } from "@/trigger/adjustAllowance.js"; +import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; +import { EntityService } from "../EntityService.js"; export const handleDeleteEntity = async (req: any, res: any) => { try { - const { org, env, db, logtail: logger, features } = req; + const { org, env, db, logger, features } = req; const { customer_id, entity_id } = req.params; await handleCustomerRaceCondition({ @@ -73,9 +73,9 @@ export const handleDeleteEntity = async (req: any, res: any) => { const feature = features.find((f: any) => f.id === entity?.feature_id); for (const cusProduct of cusProducts) { - let cusEnts = cusProduct.customer_entitlements; + const cusEnts = cusProduct.customer_entitlements; - let mainCusEnt = findMainCusEntForFeature({ + const mainCusEnt = findMainCusEntForFeature({ cusEnts, feature, }); @@ -97,12 +97,12 @@ export const handleDeleteEntity = async (req: any, res: any) => { logger, }); - let linkedCusEnts = findLinkedCusEnts({ + const linkedCusEnts = findLinkedCusEnts({ cusEnts: cusProduct.customer_entitlements, feature: mainCusEnt.entitlement.feature, }); - let replaceable = + const replaceable = newReplaceables && newReplaceables.length > 0 ? newReplaceables[0] : null; @@ -121,14 +121,14 @@ export const handleDeleteEntity = async (req: any, res: any) => { for (const linkedCusEnt of linkedCusEnts) { let newEntities; if (replaceable) { - let { newEntities: newEntities_ } = replaceEntityInCusEnt({ + const { newEntities: newEntities_ } = replaceEntityInCusEnt({ cusEnt: linkedCusEnt, entityId: entity.id, replaceable, }); newEntities = newEntities_; } else { - let { newEntities: newEntities_ } = deleteEntityFromCusEnt({ + const { newEntities: newEntities_ } = deleteEntityFromCusEnt({ cusEnt: linkedCusEnt, entityId: entity.id, }); diff --git a/server/src/internal/api/entitled/checkRouter.ts b/server/src/internal/api/entitled/checkRouter.ts index b75c0ee2b..506a02b56 100644 --- a/server/src/internal/api/entitled/checkRouter.ts +++ b/server/src/internal/api/entitled/checkRouter.ts @@ -27,7 +27,7 @@ checkRouter.post("", async (req: any, res: any) => { entity_id, } = req.body; - const { logtail: logger, db } = req; + const { logger, db } = req; if (!customer_id) { throw new RecaseError({ diff --git a/server/src/internal/api/entitled/handlers/handleProductCheck.ts b/server/src/internal/api/entitled/handlers/handleProductCheck.ts index ccc5fbdd6..e789b13b8 100644 --- a/server/src/internal/api/entitled/handlers/handleProductCheck.ts +++ b/server/src/internal/api/entitled/handlers/handleProductCheck.ts @@ -1,8 +1,11 @@ -import { CusProductStatus, FullCusProduct, SuccessCode } from "@autumn/shared"; -import { notNullish } from "@/utils/genUtils.js"; -import { ProductService } from "@/internal/products/ProductService.js"; +import { + CusProductStatus, + type FullCusProduct, + SuccessCode, +} from "@autumn/shared"; import { getOrCreateCustomer } from "@/internal/customers/cusUtils/getOrCreateCustomer.js"; -import { getOrgAndFeatures } from "@/internal/orgs/orgUtils.js"; +import { ProductService } from "@/internal/products/ProductService.js"; +import { notNullish } from "@/utils/genUtils.js"; import { getProductCheckPreview } from "./getProductCheckPreview.js"; @@ -21,12 +24,10 @@ export const handleProductCheck = async ({ with_preview, entity_data, } = req.body; - const { orgId, env, logtail: logger, db } = req; - - let { org, features } = await getOrgAndFeatures({ req }); + const { orgId, env, logger, db } = req; // 1. Get customer and org - let [customer, product] = await Promise.all([ + const [customer, product] = await Promise.all([ getOrCreateCustomer({ req, customerId: customer_id, @@ -53,15 +54,15 @@ export const handleProductCheck = async ({ if (customer.entity) { cusProducts = cusProducts.filter( (cusProduct: FullCusProduct) => - cusProduct.internal_entity_id == customer.entity!.internal_id, + cusProduct.internal_entity_id === customer.entity!.internal_id, ); } - let cusProduct: FullCusProduct | undefined = cusProducts.find( + const cusProduct: FullCusProduct | undefined = cusProducts.find( (cusProduct: FullCusProduct) => cusProduct.product.id === product_id, ); - let preview = with_preview + const preview = with_preview ? await getProductCheckPreview({ req, customer, @@ -96,7 +97,7 @@ export const handleProductCheck = async ({ return; } - let onTrial = + const onTrial = notNullish(cusProduct.trial_ends_at) && cusProduct.trial_ends_at! > Date.now(); diff --git a/server/src/internal/api/events/eventRouter.ts b/server/src/internal/api/events/eventRouter.ts index 9aa594704..a0558f96d 100644 --- a/server/src/internal/api/events/eventRouter.ts +++ b/server/src/internal/api/events/eventRouter.ts @@ -178,7 +178,7 @@ export const handleEventSent = async ({ customer_id, customer_data, event_data, - logger: req.logtail, + logger: req.logger, entityId: event_data.entity_id, entityData: event_data.entity_data, features, diff --git a/server/src/internal/api/events/usageRouter.ts b/server/src/internal/api/events/usageRouter.ts index 92378b7ba..cca4a1ef8 100644 --- a/server/src/internal/api/events/usageRouter.ts +++ b/server/src/internal/api/events/usageRouter.ts @@ -141,7 +141,7 @@ export const handleUsageEvent = async ({ entity_id, idempotency_key, } = req.body; - const { logtail: logger } = req; + const { logger } = req; if (!customer_id || !feature_id) { throw new RecaseError({ diff --git a/server/src/internal/api/invoiceRouter.ts b/server/src/internal/api/invoiceRouter.ts index 3a9222ffc..219a59939 100644 --- a/server/src/internal/api/invoiceRouter.ts +++ b/server/src/internal/api/invoiceRouter.ts @@ -1,8 +1,7 @@ -import { createStripeCli } from "@/external/stripe/utils.js"; - +import { Router } from "express"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; import { handleRequestError } from "@/utils/errorUtils.js"; -import { Router } from "express"; export const invoiceRouter: Router = Router(); diff --git a/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts b/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts index 4f93b0360..7f65fde10 100644 --- a/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts +++ b/server/src/internal/api/rewards/handlers/referrals/handleRedeemReferral.ts @@ -10,8 +10,8 @@ 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 { triggerRedemption } from "@/internal/rewards/referralUtils.js"; import { triggerFreeProduct } from "@/internal/rewards/referralUtils/triggerFreeProduct.js"; +import { 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"; @@ -24,7 +24,7 @@ export default async (req: any, res: any) => res, action: "redeem referral code", handler: async (req, res) => { - const { orgId, env, logtail: logger, db } = req; + const { orgId, env, logger, db } = req; const { code, customer_id: customerId } = req.body; // 1. Get redeemed by customer, and referral code diff --git a/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts index 39f6c406e..fcb18b1fd 100644 --- a/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts +++ b/server/src/internal/api/rewards/handlers/rewards/handleCreateCoupon.ts @@ -23,7 +23,7 @@ export default async (req: any, res: any) => res, action: "create coupon", handler: async (req, res) => { - const { db, orgId, env, logtail: logger } = req; + const { db, orgId, env, logger } = req; const rewardBody = req.body; const rewardData = CreateRewardSchema.parse(rewardBody); diff --git a/server/src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts index c22341b90..e2561ee67 100644 --- a/server/src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts +++ b/server/src/internal/api/rewards/handlers/rewards/handleDeleteCoupon.ts @@ -1,5 +1,5 @@ import { ErrCode } from "@autumn/shared"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; import { RewardService } from "@/internal/rewards/RewardService.js"; import RecaseError from "@/utils/errorUtils.js"; diff --git a/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts b/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts index 2c13271b8..d8b2e060e 100644 --- a/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts +++ b/server/src/internal/api/rewards/handlers/rewards/handleUpdateCoupon.ts @@ -1,6 +1,6 @@ import { ErrCode, PriceType, RewardCategory } from "@autumn/shared"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; 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"; @@ -16,7 +16,7 @@ export default async (req: any, res: any) => action: "update coupon", handler: async (req, res) => { const { internalId } = req.params; - const { orgId, env, db, logtail: logger } = req; + const { orgId, env, db, logger } = req; const rewardBody = req.body; const org = await OrgService.getFromReq(req); diff --git a/server/src/internal/auth/UserService.ts b/server/src/internal/auth/UserService.ts new file mode 100644 index 000000000..b9019f449 --- /dev/null +++ b/server/src/internal/auth/UserService.ts @@ -0,0 +1,11 @@ +import { user as userTable } from "@autumn/shared"; +import { eq } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; + +export class UserService { + static async getByEmail({ db, email }: { db: DrizzleCli; email: string }) { + return await db.query.user.findFirst({ + where: eq(userTable.email, email), + }); + } +} diff --git a/server/src/internal/customers/add-product/handleCreateCheckout.ts b/server/src/internal/customers/add-product/handleCreateCheckout.ts index ac7a69920..06fb5e9d3 100644 --- a/server/src/internal/customers/add-product/handleCreateCheckout.ts +++ b/server/src/internal/customers/add-product/handleCreateCheckout.ts @@ -5,8 +5,8 @@ import { SuccessCode, } from "@autumn/shared"; import type Stripe from "stripe"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { createCheckoutMetadata } from "@/internal/metadata/metadataUtils.js"; import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js"; import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js"; @@ -31,7 +31,7 @@ export const handleCreateCheckout = async ({ config: AttachConfig; returnCheckout?: boolean; }) => { - const { db, logtail: logger } = req; + const { db, logger } = req; const { customer, org, freeTrial, successUrl, rewards } = attachParams; diff --git a/server/src/internal/customers/attach/attachFunctions/handleRenewProduct.ts b/server/src/internal/customers/attach/attachFunctions/handleRenewProduct.ts index c33b5711b..3f1c69cfa 100644 --- a/server/src/internal/customers/attach/attachFunctions/handleRenewProduct.ts +++ b/server/src/internal/customers/attach/attachFunctions/handleRenewProduct.ts @@ -41,7 +41,7 @@ export const handleRenewProduct = async ({ attachParams: AttachParams; config: AttachConfig; }) => { - const logger = req.logtail; + const logger = req.logger; const { stripeCli } = attachParams; let { curScheduledProduct } = attachParamToCusProducts({ attachParams }); @@ -119,6 +119,7 @@ export const handleRenewProduct = async ({ updates: { canceled: false, canceled_at: null, + ended_at: null, }, }); } else { @@ -159,6 +160,7 @@ export const handleRenewProduct = async ({ scheduled_ids: [schedule.id], canceled: false, canceled_at: null, + ended_at: null, }, }); } else { @@ -181,6 +183,7 @@ export const handleRenewProduct = async ({ updates: { canceled: false, canceled_at: null, + ended_at: null, }, }); } @@ -209,6 +212,7 @@ export const handleRenewProduct = async ({ updates: { canceled: false, canceled_at: null, + ended_at: null, }, }); } diff --git a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts index dc5b0a9ff..538e25dce 100644 --- a/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts +++ b/server/src/internal/customers/attach/attachFunctions/scheduleFlow/handleScheduleFlow2.ts @@ -8,6 +8,7 @@ import { } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import { subItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js"; import { @@ -29,7 +30,6 @@ import { paramsToScheduleItems } from "../../mergeUtils/paramsToScheduleItems.js import { getCurrentPhaseIndex } from "../../mergeUtils/phaseUtils/phaseUtils.js"; import { subToNewSchedule } from "../../mergeUtils/subToNewSchedule.js"; import { updateCurSchedule } from "../../mergeUtils/updateCurSchedule.js"; -import { subItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; export const handleScheduleFunction2 = async ({ req, @@ -81,8 +81,10 @@ export const handleScheduleFunction2 = async ({ subItemInCusProduct({ cusProduct: curCusProduct, subItem: item }), ); - if (subItems.length == 0) { - logger.error(`SCHEDULE FLOW: subItems is empty, curCusProduct: ${curCusProduct.product.name}`); + if (subItems.length === 0) { + logger.error( + `SCHEDULE FLOW: subItems is empty, curCusProduct: ${curCusProduct.product.name}`, + ); throw new InternalError({ message: `SCHEDULE FLOW: subItems is empty, curCusProduct: ${curCusProduct.product.name}`, }); diff --git a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts index fde5fef69..e499442d6 100644 --- a/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts +++ b/server/src/internal/customers/attach/attachFunctions/upgradeFlow/handleUpgradeFlow.ts @@ -177,6 +177,7 @@ export const handleUpgradeFlow = async ({ updates: { subscription_ids: canceled ? undefined : [], status: CusProductStatus.Expired, + ended_at: Date.now(), }, }); @@ -229,8 +230,6 @@ export const handleUpgradeFlow = async ({ } if (res) { - - if (req.apiVersion.gte(ApiVersion.V1_1)) { res.status(200).json( AttachResultSchema.parse({ diff --git a/server/src/internal/customers/attach/attachRouter.ts b/server/src/internal/customers/attach/attachRouter.ts index 298528192..c4e13866f 100644 --- a/server/src/internal/customers/attach/attachRouter.ts +++ b/server/src/internal/customers/attach/attachRouter.ts @@ -5,12 +5,12 @@ import { type FullCusProduct, } from "@autumn/shared"; import { Router } from "express"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js"; import { createStripeCusIfNotExists, getCusPaymentMethod, } from "@/external/stripe/stripeCusUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { CusService } from "@/internal/customers/CusService.js"; import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { @@ -140,7 +140,7 @@ export const checkStripeConnections = async ({ useCheckout?: boolean; }) => { const { org, customer, products, stripeCus, stripeCli } = attachParams; - const logger = req.logtail; + const logger = req.logger; const env = customer.env; // 2. If invoice only and no email, save email diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.ts index 50eba780f..d0a597716 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/checkToAttachParams.ts @@ -1,5 +1,5 @@ import type { FullCustomer, FullProduct } from "@autumn/shared"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { getFreeTrialAfterFingerprint } from "@/internal/products/free-trials/freeTrialUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts b/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts index e886e2eba..8493b4fb1 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/convertToParams.ts @@ -12,7 +12,7 @@ import { type Organization, } from "@autumn/shared"; import type Stripe from "stripe"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import type { AttachParams, InsertCusProductParams, diff --git a/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts b/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts index 979d99b79..d74125db5 100644 --- a/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts +++ b/server/src/internal/customers/attach/attachUtils/attachParams/processAttachBody.ts @@ -1,6 +1,6 @@ import { type AttachBody, ErrCode } from "@autumn/shared"; import type Stripe from "stripe"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { RewardService } from "@/internal/rewards/RewardService.js"; import RecaseError from "@/utils/errorUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; diff --git a/server/src/internal/customers/attach/handleSetupPayment.ts b/server/src/internal/customers/attach/handleSetupPayment.ts index 2237673e3..1b9f29d13 100644 --- a/server/src/internal/customers/attach/handleSetupPayment.ts +++ b/server/src/internal/customers/attach/handleSetupPayment.ts @@ -1,6 +1,6 @@ import { ErrCode } from "@autumn/shared"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js"; import RecaseError from "@/utils/errorUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; diff --git a/server/src/internal/customers/cancel/cancelEndOfCycle.ts b/server/src/internal/customers/cancel/cancelEndOfCycle.ts index 65bdef876..681e07b1f 100644 --- a/server/src/internal/customers/cancel/cancelEndOfCycle.ts +++ b/server/src/internal/customers/cancel/cancelEndOfCycle.ts @@ -1,9 +1,13 @@ -import { createStripeCli } from "@/external/stripe/utils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { CusProductStatus, FullCusProduct, FullCustomer } from "@autumn/shared"; -import { cusProductToSub } from "../cusProducts/cusProductUtils/convertCusProduct.js"; +import { + CusProductStatus, + type FullCusProduct, + type FullCustomer, +} from "@autumn/shared"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { getLatestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; import { CusProductService } from "../cusProducts/CusProductService.js"; +import { cusProductToSub } from "../cusProducts/cusProductUtils/convertCusProduct.js"; export const cancelEndOfCycle = async ({ req, diff --git a/server/src/internal/customers/cancel/cancelImmediately.ts b/server/src/internal/customers/cancel/cancelImmediately.ts index 69b9550c0..101d3d369 100644 --- a/server/src/internal/customers/cancel/cancelImmediately.ts +++ b/server/src/internal/customers/cancel/cancelImmediately.ts @@ -1,20 +1,18 @@ -import { createStripeCli } from "@/external/stripe/utils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; import { AttachScenario, CusProductStatus, - FullCusProduct, - FullCustomer, + cusProductToProduct, + type FullCusProduct, + type FullCustomer, } from "@autumn/shared"; - -import { cusProductToProduct } from "@autumn/shared"; - -import { CusProductService } from "../cusProducts/CusProductService.js"; -import { activateDefaultProduct } from "../cusProducts/cusProductUtils.js"; -import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; import { isOneOff } from "@/internal/products/productUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { CusProductService } from "../cusProducts/CusProductService.js"; import { cusProductToSub } from "../cusProducts/cusProductUtils/convertCusProduct.js"; +import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js"; +import { activateDefaultProduct } from "../cusProducts/cusProductUtils.js"; export const cancelImmediately = async ({ req, diff --git a/server/src/internal/customers/cancel/cancelScheduledProduct.ts b/server/src/internal/customers/cancel/cancelScheduledProduct.ts index b1f3368e7..1453250b8 100644 --- a/server/src/internal/customers/cancel/cancelScheduledProduct.ts +++ b/server/src/internal/customers/cancel/cancelScheduledProduct.ts @@ -1,6 +1,10 @@ -import { createStripeCli } from "@/external/stripe/utils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { FullCusProduct, FullCustomer, CusProductStatus } from "@autumn/shared"; +import { + CusProductStatus, + type FullCusProduct, + type FullCustomer, +} from "@autumn/shared"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; import { CusProductService } from "../cusProducts/CusProductService.js"; import { cusProductToSchedule } from "../cusProducts/cusProductUtils/convertCusProduct.js"; diff --git a/server/src/internal/customers/cancel/handleCancelProduct.ts b/server/src/internal/customers/cancel/handleCancelProduct.ts index e8990831a..0fde911b6 100644 --- a/server/src/internal/customers/cancel/handleCancelProduct.ts +++ b/server/src/internal/customers/cancel/handleCancelProduct.ts @@ -11,7 +11,7 @@ import { ProrationBehavior, } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; import RecaseError from "@/utils/errorUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; diff --git a/server/src/internal/customers/cusCache/updateCachedCus.ts b/server/src/internal/customers/cusCache/updateCachedCus.ts index c392ba9e5..5fc8899cb 100644 --- a/server/src/internal/customers/cusCache/updateCachedCus.ts +++ b/server/src/internal/customers/cusCache/updateCachedCus.ts @@ -1,14 +1,10 @@ -import { - CusExpand, - FullCusEntWithFullCusProduct, - Organization, -} from "@autumn/shared"; -import { AppEnv } from "autumn-js"; +import { type CusExpand, type Organization } from "@autumn/shared"; +import type { AppEnv } from "autumn-js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { logger } from "@/external/logtail/logtailUtils.js"; import { buildBaseCusCacheKey } from "./cusCacheUtils.js"; import { getCusWithCache } from "./getCusWithCache.js"; import { initUpstash } from "./upstashUtils.js"; -import { logger } from "@/external/logtail/logtailUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; export const refreshCusCache = async ({ db, @@ -43,14 +39,14 @@ export const refreshCusCache = async ({ for (const key of list) { const refresh = async () => { const keyName = key; - let params = keyName.split(":"); - let expandParam = params.find((p) => p.startsWith("expand_")); - let expand = expandParam + const params = keyName.split(":"); + const expandParam = params.find((p) => p.startsWith("expand_")); + const expand = expandParam ? expandParam.replace("expand_", "").split(",") : []; - let entityIdParam = params.find((p) => p.startsWith("entity_")); - let entityId = entityIdParam + const entityIdParam = params.find((p) => p.startsWith("entity_")); + const entityId = entityIdParam ? entityIdParam.replace("entity_", "") : undefined; diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts index d246612e5..6cbe487d9 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts @@ -15,7 +15,7 @@ import { } from "@autumn/shared"; import { logger } from "better-auth"; import { Decimal } from "decimal.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { getEntOptions } from "@/internal/products/prices/priceUtils.js"; import { notNullish, nullish } from "@/utils/genUtils.js"; import { diff --git a/server/src/internal/customers/cusProducts/cusProductUtils.ts b/server/src/internal/customers/cusProducts/cusProductUtils.ts index 6029d1f84..43cf86d8f 100644 --- a/server/src/internal/customers/cusProducts/cusProductUtils.ts +++ b/server/src/internal/customers/cusProducts/cusProductUtils.ts @@ -19,8 +19,8 @@ import { type UsagePriceConfig, } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { diff --git a/server/src/internal/customers/cusRouter.ts b/server/src/internal/customers/cusRouter.ts index 6fa94b9d7..a866e3cef 100644 --- a/server/src/internal/customers/cusRouter.ts +++ b/server/src/internal/customers/cusRouter.ts @@ -2,8 +2,8 @@ import { ErrCode } from "@autumn/shared"; import { Router } from "express"; import { Hono } from "hono"; import { StatusCodes } from "http-status-codes"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; import { CusSearchService } from "@/internal/customers/CusSearchService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; @@ -101,7 +101,7 @@ expressCusRouter.get( org, env: req.env, customer, - logger: req.logtail, + logger: req.logger, }); if (!newCus) { diff --git a/server/src/internal/customers/cusUtils/createNewCustomer.ts b/server/src/internal/customers/cusUtils/createNewCustomer.ts index a329df627..191ccc483 100644 --- a/server/src/internal/customers/cusUtils/createNewCustomer.ts +++ b/server/src/internal/customers/cusUtils/createNewCustomer.ts @@ -8,7 +8,7 @@ import { type FullCustomer, type FullProduct, } from "@autumn/shared"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { addCustomerCreatedTask } from "@/internal/analytics/handlers/handleCustomerCreated.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js"; diff --git a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusPaymentMethodRes.ts b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusPaymentMethodRes.ts index 138fb8693..3f98cc8f7 100644 --- a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusPaymentMethodRes.ts +++ b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusPaymentMethodRes.ts @@ -1,7 +1,11 @@ +import { + type AppEnv, + CusExpand, + type FullCustomer, + type Organization, +} from "@autumn/shared"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { getCusPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { AppEnv, CusExpand, FullCustomer, Organization } from "@autumn/shared"; export const getCusPaymentMethodRes = async ({ org, @@ -18,12 +22,12 @@ export const getCusPaymentMethodRes = async ({ return undefined; } - let stripeCli = createStripeCli({ + const stripeCli = createStripeCli({ org, env, }); - let paymentMethod = await getCusPaymentMethod({ + const paymentMethod = await getCusPaymentMethod({ stripeCli, stripeId: fullCus.processor?.id, errorIfNone: false, diff --git a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusRewards.ts b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusRewards.ts index ac5770655..9618be7b0 100644 --- a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusRewards.ts +++ b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusRewards.ts @@ -7,8 +7,8 @@ import { RewardType, } from "@autumn/shared"; import type Stripe from "stripe"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; export const getCusRewards = async ({ org, diff --git a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusUpcomingInvoice.ts b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusUpcomingInvoice.ts index f68c01951..6b90bc4bd 100644 --- a/server/src/internal/customers/cusUtils/cusResponseUtils/getCusUpcomingInvoice.ts +++ b/server/src/internal/customers/cusUtils/cusResponseUtils/getCusUpcomingInvoice.ts @@ -8,10 +8,10 @@ import { } from "@autumn/shared"; import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { getEarliestPeriodEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; import { lineItemInCusProduct } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { stripeDiscountToResponse } from "./stripeDiscountToResponse.js"; export const getCusUpcomingInvoice = async ({ diff --git a/server/src/internal/customers/handlers/handleAddCouponToCus.ts b/server/src/internal/customers/handlers/handleAddCouponToCus.ts index 2f55186ea..9359b5825 100644 --- a/server/src/internal/customers/handlers/handleAddCouponToCus.ts +++ b/server/src/internal/customers/handlers/handleAddCouponToCus.ts @@ -1,7 +1,7 @@ import { ErrCode } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { CusService } from "@/internal/customers/CusService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; import { RewardService } from "@/internal/rewards/RewardService.js"; diff --git a/server/src/internal/customers/handlers/handleCreateBillingPortal.ts b/server/src/internal/customers/handlers/handleCreateBillingPortal.ts index 2912f1e42..f3821a495 100644 --- a/server/src/internal/customers/handlers/handleCreateBillingPortal.ts +++ b/server/src/internal/customers/handlers/handleCreateBillingPortal.ts @@ -1,8 +1,8 @@ import { ErrCode } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; import type Stripe from "stripe"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createStripeCusIfNotExists } from "@/external/stripe/stripeCusUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { CusService } from "@/internal/customers/CusService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; import { toSuccessUrl } from "@/internal/orgs/orgUtils/convertOrgUtils.js"; @@ -81,7 +81,7 @@ export const handleCreateBillingPortal = async (req: any, res: any) => org, env: req.env, customer, - logger: req.logtail, + logger: req.logger, }); if (!newCus) { @@ -120,14 +120,14 @@ export const handleCreateBillingPortal = async (req: any, res: any) => ) { try { // Create a default billing portal configuration - req.logtail?.info( + req.logger?.info( `Creating default billing portal configuration for customer ${customer.id}`, ); const configuration = await createDefaultBillingPortalConfiguration(stripeCli); - req.logtail?.info( + req.logger?.info( "Successfully created billing portal configuration", { configurationId: configuration.id, @@ -142,13 +142,10 @@ export const handleCreateBillingPortal = async (req: any, res: any) => configuration: configuration.id, }); } catch (configError: any) { - req.logtail?.error( - "Failed to create billing portal configuration", - { - error: configError.message, - orgId: org.id, - }, - ); + req.logger?.error("Failed to create billing portal configuration", { + error: configError.message, + orgId: org.id, + }); throw new RecaseError({ message: `Failed to create billing portal configuration: ${configError.message}`, code: ErrCode.StripeError, diff --git a/server/src/internal/customers/handlers/handleCusProductExpired.ts b/server/src/internal/customers/handlers/handleCusProductExpired.ts index 16579ba4f..34b530c43 100644 --- a/server/src/internal/customers/handlers/handleCusProductExpired.ts +++ b/server/src/internal/customers/handlers/handleCusProductExpired.ts @@ -1,30 +1,24 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js"; +import { + CusProductStatus, + cusProductToPrices, + ErrCode, + type FullCusProduct, + type FullCustomer, +} from "@autumn/shared"; +import { StatusCodes } from "http-status-codes"; import { ACTIVE_STATUSES, CusProductService, } from "@/internal/customers/cusProducts/CusProductService.js"; +import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js"; import { cancelCusProductSubscriptions, expireAndActivate, - fullCusProductToProduct, } from "@/internal/customers/cusProducts/cusProductUtils.js"; import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { - ErrCode, - CusProductStatus, - FullCusProduct, - Organization, - AppEnv, - FullCustomer, -} from "@autumn/shared"; -import { StatusCodes } from "http-status-codes"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; import { CusService } from "../CusService.js"; -import { cusProductToPrices } from "@autumn/shared"; - export const expireCusProduct = async ({ req, cusProduct, // cus product to expire @@ -61,8 +55,8 @@ export const expireCusProduct = async ({ // } // 1. If main product, can't expire if there's scheduled product - let isMain = !cusProduct.product.is_add_on; - let { curScheduledProduct: futureProduct } = getExistingCusProducts({ + const isMain = !cusProduct.product.is_add_on; + const { curScheduledProduct: futureProduct } = getExistingCusProducts({ product: cusProduct.product, cusProducts: fullCus.customer_products, internalEntityId: cusProduct.internal_entity_id, @@ -173,7 +167,7 @@ export const handleCusProductExpired = async (req: any, res: any) => { const { db } = req; const customerProductId = req.params.customer_product_id; - let cusProduct = await CusProductService.get({ + const cusProduct = await CusProductService.get({ db, id: customerProductId, orgId: req.orgId, diff --git a/server/src/internal/customers/handlers/handleDeleteCustomer.ts b/server/src/internal/customers/handlers/handleDeleteCustomer.ts index 12ff519a3..53282677b 100644 --- a/server/src/internal/customers/handlers/handleDeleteCustomer.ts +++ b/server/src/internal/customers/handlers/handleDeleteCustomer.ts @@ -1,12 +1,15 @@ +import { type AppEnv, ErrCode, type Organization } from "@autumn/shared"; import chalk from "chalk"; -import RecaseError from "@/utils/errorUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { StatusCodes } from "http-status-codes"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { deleteStripeCustomer } from "@/external/stripe/stripeCusUtils.js"; import { CusService } from "@/internal/customers/CusService.js"; -import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js"; +import RecaseError from "@/utils/errorUtils.js"; +import type { + ExtendedRequest, + ExtendedResponse, +} from "@/utils/models/Request.js"; import { routeHandler } from "@/utils/routerUtils.js"; -import { AppEnv, ErrCode, Organization } from "@autumn/shared"; -import { StatusCodes } from "http-status-codes"; export const deleteCusById = async ({ db, @@ -40,7 +43,7 @@ export const deleteCusById = async ({ }); } - let response = { + const response = { customer, success: true, }; @@ -80,7 +83,7 @@ export const handleDeleteCustomer = async (req: any, res: any) => res, action: "delete customer", handler: async (req: ExtendedRequest, res: ExtendedResponse) => { - const { env, logtail: logger, db, org } = req; + const { env, logger, db, org } = req; const { delete_in_stripe } = req.query; const data = await deleteCusById({ diff --git a/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts b/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts index 63b526090..cd87f7187 100644 --- a/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts +++ b/server/src/internal/customers/handlers/handleTransferProduct/handleDecreaseAndTransfer.ts @@ -7,7 +7,7 @@ import { type FullCustomer, getStartingBalance, } from "@autumn/shared"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { getEntOptions } from "@/internal/products/prices/priceUtils.js"; import { attachToInsertParams } from "@/internal/products/productUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; diff --git a/server/src/internal/customers/handlers/handleUpdateBalances.ts b/server/src/internal/customers/handlers/handleUpdateBalances.ts index 182bd80f7..63d7c6d82 100644 --- a/server/src/internal/customers/handlers/handleUpdateBalances.ts +++ b/server/src/internal/customers/handlers/handleUpdateBalances.ts @@ -1,22 +1,18 @@ -import { handleRequestError } from "@/utils/errorUtils.js"; - -import { CusService } from "@/internal/customers/CusService.js"; -import { FeatureService } from "@/internal/features/FeatureService.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { ErrCode } from "@autumn/shared"; -import { StatusCodes } from "http-status-codes"; -import { getCusEntsInFeatures } from "../cusUtils/cusUtils.js"; +import { ErrCode, getCusEntBalance } from "@autumn/shared"; import { Decimal } from "decimal.js"; +import { StatusCodes } from "http-status-codes"; +import { CusService } from "@/internal/customers/CusService.js"; +import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; +import { getUnlimitedAndUsageAllowed } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; +import { FeatureService } from "@/internal/features/FeatureService.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; import { deductAllowanceFromCusEnt, deductFromUsageBasedCusEnt, } from "@/trigger/updateBalanceTask.js"; -import { OrgService } from "@/internal/orgs/OrgService.js"; - -import { getUnlimitedAndUsageAllowed } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; -import { getCusEntBalance } from "@autumn/shared"; -import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; +import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; import { notNullish } from "@/utils/genUtils.js"; +import { getCusEntsInFeatures } from "../cusUtils/cusUtils.js"; const getCusFeaturesAndOrg = async (req: any, customerId: string) => { // 1. Get customer @@ -45,7 +41,7 @@ const getCusFeaturesAndOrg = async (req: any, customerId: string) => { export const handleUpdateBalances = async (req: any, res: any) => { try { - const logger = req.logtail; + const logger = req.logger; const cusId = req.params.customer_id; const { env, db, features } = req; const { balances } = req.body; @@ -75,7 +71,7 @@ export const handleUpdateBalances = async (req: any, res: any) => { const { cusEnts, cusPrices } = await getCusEntsInFeatures({ customer, internalFeatureIds: featuresToUpdate.map((f: any) => f.internal_id!), - logger: req.logtail, + logger: req.logger, }); logger.info("--------------------------------"); @@ -121,7 +117,7 @@ export const handleUpdateBalances = async (req: any, res: any) => { continue; } - let { unlimited } = getUnlimitedAndUsageAllowed({ + const { unlimited } = getUnlimitedAndUsageAllowed({ cusEnts, internalFeatureId: feature!.internal_id!, }); @@ -135,21 +131,21 @@ export const handleUpdateBalances = async (req: any, res: any) => { } // Get deductions - let newBalance = balance.balance; + const newBalance = balance.balance; let curBalance = new Decimal(0); - let properties = structuredClone(balance); + const properties = structuredClone(balance); delete properties.feature_id; delete properties.balance; for (const cusEnt of cusEnts) { - let cusEntIntCount = cusEnt.entitlement.interval_count || 1; - let deductionIntCount = balance.interval_count || 1; + const cusEntIntCount = cusEnt.entitlement.interval_count || 1; + const deductionIntCount = balance.interval_count || 1; - let intCountMatch = notNullish(balance.interval_count) + const intCountMatch = notNullish(balance.interval_count) ? cusEntIntCount === deductionIntCount : true; - let intMatch = notNullish(balance.interval) + const intMatch = notNullish(balance.interval) ? balance.interval === cusEnt.entitlement.interval : true; @@ -161,7 +157,7 @@ export const handleUpdateBalances = async (req: any, res: any) => { continue; } - let { balance: cusEntBalance } = getCusEntBalance({ + const { balance: cusEntBalance } = getCusEntBalance({ cusEnt, entityId: balance.entity_id, }); @@ -169,7 +165,7 @@ export const handleUpdateBalances = async (req: any, res: any) => { curBalance = curBalance.add(new Decimal(cusEntBalance!)); } - let toDeduct = curBalance.sub(newBalance).toNumber(); + const toDeduct = curBalance.sub(newBalance).toNumber(); if (toDeduct == 0) { logger.info(`Skipping ${feature!.id} -- no change`); @@ -197,8 +193,8 @@ export const handleUpdateBalances = async (req: any, res: any) => { const cusEnt = notNullish(interval) ? cusEnts.find((cusEnt) => { - let cusEntIntCount = cusEnt.entitlement.interval_count || 1; - let deductionIntCount = featureDeduction.intervalCount || 1; + const cusEntIntCount = cusEnt.entitlement.interval_count || 1; + const deductionIntCount = featureDeduction.intervalCount || 1; return ( cusEnt.internal_feature_id === feature!.internal_id! && @@ -233,14 +229,14 @@ export const handleUpdateBalances = async (req: any, res: any) => { } for (const cusEnt of cusEnts) { - let cusEntIntCount = cusEnt.entitlement.interval_count || 1; - let deductionIntCount = featureDeduction.intervalCount || 1; + const cusEntIntCount = cusEnt.entitlement.interval_count || 1; + const deductionIntCount = featureDeduction.intervalCount || 1; - let intCountMatch = notNullish(featureDeduction.intervalCount) + const intCountMatch = notNullish(featureDeduction.intervalCount) ? cusEntIntCount === deductionIntCount : true; - let intMatch = notNullish(featureDeduction.interval) + const intMatch = notNullish(featureDeduction.interval) ? featureDeduction.interval === cusEnt.entitlement.interval : true; diff --git a/server/src/internal/customers/handlers/handleUpdateCustomer.ts b/server/src/internal/customers/handlers/handleUpdateCustomer.ts index a1170b4e8..b7eb8ec87 100644 --- a/server/src/internal/customers/handlers/handleUpdateCustomer.ts +++ b/server/src/internal/customers/handlers/handleUpdateCustomer.ts @@ -1,6 +1,6 @@ import { CreateCustomerSchema, ErrCode, ProcessorType } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { CusService } from "@/internal/customers/CusService.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; import RecaseError from "@/utils/errorUtils.js"; @@ -139,7 +139,7 @@ export const handleUpdateCustomer = async (req: any, res: any) => customer: finalCustomer, org, env: req.env, - logger: req.logtail, + logger: req.logger, cusProducts: finalCustomer.customer_products, expand: parseCusExpand(req.query.expand as string), features, diff --git a/server/src/internal/customers/handlers/handleUpdateEntitlement.ts b/server/src/internal/customers/handlers/handleUpdateEntitlement.ts index 9c2360543..89abfba2a 100644 --- a/server/src/internal/customers/handlers/handleUpdateEntitlement.ts +++ b/server/src/internal/customers/handlers/handleUpdateEntitlement.ts @@ -1,18 +1,19 @@ -import { handleRequestError } from "@/utils/errorUtils.js"; - -import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { ErrCode, FullCustomerEntitlement } from "@autumn/shared"; +import { + ErrCode, + type FullCustomerEntitlement, + getCusEntBalance, +} from "@autumn/shared"; import { Decimal } from "decimal.js"; import { StatusCodes } from "http-status-codes"; -import { adjustAllowance } from "@/trigger/adjustAllowance.js"; -import { CusPriceService } from "@/internal/customers/cusProducts/cusPrices/CusPriceService.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { CusService } from "@/internal/customers/CusService.js"; +import { CusEntService } from "@/internal/customers/cusProducts/cusEnts/CusEntitlementService.js"; +import { CusPriceService } from "@/internal/customers/cusProducts/cusPrices/CusPriceService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; -import { getCusEntBalance } from "@autumn/shared"; +import { adjustAllowance } from "@/trigger/adjustAllowance.js"; import { performDeductionOnCusEnt } from "@/trigger/updateBalanceTask.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; import { CusProductService } from "../cusProducts/CusProductService.js"; const getCusOrgAndCusPrice = async ({ @@ -41,7 +42,7 @@ const getCusOrgAndCusPrice = async ({ export const handleUpdateEntitlement = async (req: any, res: any) => { try { - const { db, logtail: logger } = req; + const { db } = req; const { customer_entitlement_id } = req.params; const { balance, next_reset_at, entity_id } = req.body; @@ -96,27 +97,29 @@ export const handleUpdateEntitlement = async (req: any, res: any) => { }); } - let { balance: masterBalance } = getCusEntBalance({ + const { balance: masterBalance } = getCusEntBalance({ cusEnt, entityId: entity_id, }); const deducted = new Decimal(masterBalance!).minus(balance).toNumber(); - let originalBalance = structuredClone(masterBalance); + const originalBalance = structuredClone(masterBalance); - let { newBalance, newEntities, newAdjustment } = performDeductionOnCusEnt({ - cusEnt: { - ...cusEnt, - customer_product: cusProduct!, + const { newBalance, newEntities, newAdjustment } = performDeductionOnCusEnt( + { + cusEnt: { + ...cusEnt, + customer_product: cusProduct!, + }, + toDeduct: deducted, + addAdjustment: true, + allowNegativeBalance: cusEnt.usage_allowed || false, + entityId: entity_id, }, - toDeduct: deducted, - addAdjustment: true, - allowNegativeBalance: cusEnt.usage_allowed || false, - entityId: entity_id, - }); + ); - let updates = { + const updates = { balance: newBalance, next_reset_at, entities: newEntities, @@ -130,7 +133,7 @@ export const handleUpdateEntitlement = async (req: any, res: any) => { }); if (cusPrice && customer) { - let fullCusProduct = await CusProductService.get({ + const fullCusProduct = await CusProductService.get({ db, id: cusEnt.customer_product_id, orgId: req.orgId, @@ -150,7 +153,7 @@ export const handleUpdateEntitlement = async (req: any, res: any) => { customer: customer, originalBalance: originalBalance!, newBalance: balance, - logger: req.logtail, + logger: req.logger, }); if (newReplaceables && newReplaceables.length > 0) { diff --git a/server/src/internal/customers/internalCusRouter.ts b/server/src/internal/customers/internalCusRouter.ts index c37616185..dc7c433ff 100644 --- a/server/src/internal/customers/internalCusRouter.ts +++ b/server/src/internal/customers/internalCusRouter.ts @@ -1,27 +1,28 @@ -import { Router } from "express"; -import { CusService } from "./CusService.js"; -import { ProductService } from "../products/ProductService.js"; - import { CusExpand, CusProductStatus, + cusProductToProduct, ErrCode, - FullCusProduct, productToCusProduct + type FullCusProduct, + productToCusProduct, } from "@autumn/shared"; - -import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js"; -import { EventService } from "../api/events/EventService.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { mapToProductV2 } from "../products/productV2Utils.js"; -import { RewardRedemptionService } from "../rewards/RewardRedemptionService.js"; -import { CusReadService } from "./CusReadService.js"; +import { Router } from "express"; +import { Hono } from "hono"; import { StatusCodes } from "http-status-codes"; -import { cusProductToProduct } from "@autumn/shared"; -import { isStripeConnected } from "../orgs/orgUtils.js"; +import { z } from "zod/v4"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; +import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js"; import { routeHandler } from "@/utils/routerUtils.js"; -import { CusSearchService } from "./CusSearchService.js"; import { CusBatchService } from "../api/batch/CusBatchService.js"; +import { EventService } from "../api/events/EventService.js"; +import { ProductService } from "../products/ProductService.js"; +import { mapToProductV2 } from "../products/productV2Utils.js"; +import { CusSearchService } from "./CusSearchService.js"; +import { CusService } from "./CusService.js"; import { ACTIVE_STATUSES } from "./cusProducts/CusProductService.js"; +import { handleGetCusReferrals } from "./internalHandlers/handleGetCusReferrals.js"; export const cusRouter: Router = Router(); @@ -49,45 +50,6 @@ cusRouter.post("/all/search", (req, res) => }), ); -// Customer page -cusRouter.get("/:customer_id", async (req: any, res: any) => { - try { - const { db, org, features, env } = req; - const { customer_id } = req.params; - const orgId = req.orgId; - - const fullCus = await CusService.getFull({ - db, - orgId, - env, - idOrInternalId: customer_id, - withEntities: true, - expand: [CusExpand.Invoices], - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Scheduled, - CusProductStatus.Expired, - ], - }); - - res.status(200).json({ - customer: fullCus, - // products: getLatestProducts(products), - // versionCounts: getProductVersionCounts(products), - // invoices, - // features, - // coupons, - // events, - // discount, - // org, - // entities, - }); - } catch (error) { - handleFrontendReqError({ req, error, res, action: "get customer data" }); - } -}); - cusRouter.get("/:customer_id/events", async (req: any, res: any) => { try { const { db, org, features, env } = req; @@ -122,89 +84,6 @@ cusRouter.get("/:customer_id/events", async (req: any, res: any) => { } }); -cusRouter.get("/:customer_id/referrals", async (req: any, res: any) => { - try { - const { env, db, org } = req; - const { customer_id } = req.params; - const orgId = req.orgId; - - let internalCustomer = await CusService.get({ - db, - orgId, - env, - idOrInternalId: customer_id, - }); - - if (!internalCustomer) { - throw new RecaseError({ - message: "Customer not found", - code: ErrCode.CustomerNotFound, - statusCode: StatusCodes.NOT_FOUND, - }); - } - - // Get all redemptions for this customer - let [referred, redeemed, stripeCus] = await Promise.all([ - RewardRedemptionService.getByReferrer({ - db, - internalCustomerId: internalCustomer.internal_id, - withCustomer: true, - limit: 100, - }), - RewardRedemptionService.getByCustomer({ - db, - internalCustomerId: internalCustomer.internal_id, - withReferralCode: true, - limit: 100, - }), - async () => { - if (isStripeConnected({ org, env }) && internalCustomer.processor?.id) { - const stripeCli = createStripeCli({ org, env }); - const stripeCus: any = await stripeCli.customers.retrieve( - internalCustomer.processor.id, - ); - return stripeCus; - } - return null; - }, - ]); - - let redeemedCustomerIds = redeemed.map( - (redemption: any) => redemption.referral_code.internal_customer_id, - ); - - let redeemedCustomers = await CusReadService.getInInternalIds({ - db, - internalIds: redeemedCustomerIds, - }); - - for (const redemption of redeemed) { - if (redemption.referral_code) { - redemption.referral_code.customer = redeemedCustomers.find( - (customer: any) => - customer.internal_id === - redemption.referral_code!.internal_customer_id, - ); - } - } - - const end = performance.now(); - - res.status(200).send({ - referred, - redeemed, - stripeCus, - }); - } catch (error) { - handleFrontendReqError({ - req, - error, - res, - action: "get customer referrals", - }); - } -}); - cusRouter.post("/all/full_customers", async (req: any, res: any) => routeHandler({ req, @@ -243,7 +122,7 @@ cusRouter.get( "/:customer_id/product/:product_id", async (req: any, res: any) => { try { - const { org, env, db, features, logtail: logger } = req; + const { org, env, db, features, logger } = req; const { customer_id, product_id } = req.params; const { version, customer_product_id, entity_id } = req.query; @@ -270,10 +149,10 @@ cusRouter.get( }); } - let cusProducts = customer.customer_products; - let entity = customer.entity; + const cusProducts = customer.customer_products; + const entity = customer.entity; - let cusProduct = productToCusProduct({ + const cusProduct = productToCusProduct({ cusProducts, productId: product_id, internalEntityId: entity?.internal_id, @@ -282,7 +161,7 @@ cusRouter.get( inStatuses: ACTIVE_STATUSES, }); - let product = cusProduct + const product = cusProduct ? cusProductToProduct({ cusProduct }) : await ProductService.getFull({ db, @@ -295,9 +174,7 @@ cusRouter.get( : undefined, }); - let productV2 = mapToProductV2({ product: product!, features }); - - + const productV2 = mapToProductV2({ product: product!, features }); res.status(200).json({ cusProduct, @@ -314,11 +191,45 @@ cusRouter.get( }, ); -cusRouter.get("/:customer_id/sub", async (req: any, res: any) => { - try { - const { org, env, db } = req; - const { customer_id } = req.params; - const orgId = req.orgId; +export const internalCusRouter = new Hono(); + +export const handleGetCustomerInternal = createRoute({ + params: z.object({ customer_id: z.string() }), + handler: async (c) => { + const { db, org, env } = c.get("ctx"); + const { customer_id } = c.req.valid("param"); + + const fullCus = await CusService.getFull({ + db, + orgId: org.id, + env, + idOrInternalId: customer_id, + withEntities: true, + expand: [CusExpand.Invoices], + inStatuses: [ + CusProductStatus.Active, + CusProductStatus.PastDue, + CusProductStatus.Scheduled, + CusProductStatus.Expired, + ], + }); + + return c.json({ + customer: fullCus, + }); + }, +}); + +export const handleGetCustomerSub = createRoute({ + params: z.object({ + customer_id: z.string(), + }), + + handler: async (c) => { + const ctx = c.get("ctx"); + const { org, env, db, logger } = ctx; + const { customer_id } = c.req.valid("param"); + const orgId = org.id; const fullCus = await CusService.getFull({ db, @@ -331,15 +242,22 @@ cusRouter.get("/:customer_id/sub", async (req: any, res: any) => { (cp: FullCusProduct) => cp.subscription_ids || [], )?.[0]; - if (!subId) return res.status(200).json({ sub: undefined }); + if (!subId) return c.json({ sub: undefined }); - const stripeCli = createStripeCli({ org, env }); - const sub = await stripeCli.subscriptions.retrieve(subId, { - expand: ["discounts.coupon"], - }); + try { + const stripeCli = createStripeCli({ org, env }); + const sub = await stripeCli.subscriptions.retrieve(subId, { + expand: ["discounts.coupon"], + }); - res.status(200).json({ sub }); - } catch (error) { - handleFrontendReqError({ req, error, res, action: "get customer rewards" }); - } + return c.json({ sub }); + } catch (error) { + logger.warn(`failed to get customers sub: ${error}`); + return c.json({ sub: undefined }); + } + }, }); + +internalCusRouter.get("/:customer_id", ...handleGetCustomerInternal); +internalCusRouter.get("/:customer_id/sub", ...handleGetCustomerSub); +internalCusRouter.get("/:customer_id/referrals", ...handleGetCusReferrals); diff --git a/server/src/internal/customers/internalHandlers/handleGetCusReferrals.ts b/server/src/internal/customers/internalHandlers/handleGetCusReferrals.ts new file mode 100644 index 000000000..293686206 --- /dev/null +++ b/server/src/internal/customers/internalHandlers/handleGetCusReferrals.ts @@ -0,0 +1,77 @@ +import { CustomerNotFoundError } from "@autumn/shared"; +import { z } from "zod/v4"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { isStripeConnected } from "@/internal/orgs/orgUtils.js"; +import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js"; +import { CusReadService } from "../CusReadService.js"; +import { CusService } from "../CusService.js"; +export const handleGetCusReferrals = createRoute({ + params: z.object({ customer_id: z.string() }), + handler: async (c) => { + const { env, db, org } = c.get("ctx"); + const { customer_id } = c.req.valid("param"); + + const internalCustomer = await CusService.get({ + db, + orgId: org.id, + env, + idOrInternalId: customer_id, + }); + + if (!internalCustomer) { + throw new CustomerNotFoundError({ customerId: customer_id }); + } + + // Get all redemptions for this customer + const [referred, redeemed, stripeCus] = await Promise.all([ + RewardRedemptionService.getByReferrer({ + db, + internalCustomerId: internalCustomer.internal_id, + withCustomer: true, + limit: 100, + }), + RewardRedemptionService.getByCustomer({ + db, + internalCustomerId: internalCustomer.internal_id, + withReferralCode: true, + limit: 100, + }), + async () => { + if (isStripeConnected({ org, env }) && internalCustomer.processor?.id) { + const stripeCli = createStripeCli({ org, env }); + const stripeCus: any = await stripeCli.customers.retrieve( + internalCustomer.processor.id, + ); + return stripeCus; + } + return null; + }, + ]); + + const redeemedCustomerIds = redeemed.map( + (redemption: any) => redemption.referral_code.internal_customer_id, + ); + + const redeemedCustomers = await CusReadService.getInInternalIds({ + db, + internalIds: redeemedCustomerIds, + }); + + for (const redemption of redeemed) { + if (redemption.referral_code) { + redemption.referral_code.customer = redeemedCustomers.find( + (customer: any) => + customer.internal_id === + redemption.referral_code!.internal_customer_id, + ); + } + } + + return c.json({ + referred, + redeemed, + stripeCus, + }); + }, +}); diff --git a/server/src/internal/dev/ApiKeyService.ts b/server/src/internal/dev/ApiKeyService.ts index e9ae6d8df..89a7ec2ef 100644 --- a/server/src/internal/dev/ApiKeyService.ts +++ b/server/src/internal/dev/ApiKeyService.ts @@ -32,6 +32,7 @@ export class ApiKeyService { features: { where: eq(features.env, env), }, + master: true, }, }, }, diff --git a/server/src/internal/dev/devRouter.ts b/server/src/internal/dev/devRouter.ts index b3c9b3daf..ed7866935 100644 --- a/server/src/internal/dev/devRouter.ts +++ b/server/src/internal/dev/devRouter.ts @@ -1,23 +1,23 @@ -import { withOrgAuth } from "@/middleware/authMiddleware.js"; import { AppEnv } from "@autumn/shared"; +import * as crypto from "crypto"; import { Router } from "express"; -import { ApiKeyService } from "./ApiKeyService.js"; -import { OrgService } from "../orgs/OrgService.js"; -import { createKey } from "./api-keys/apiKeyUtils.js"; -import { getSvixDashboardUrl } from "@/external/svix/svixHelpers.js"; -import { handleRequestError } from "@/utils/errorUtils.js"; +import type Stripe from "stripe"; import { CacheManager } from "@/external/caching/CacheManager.js"; import { CacheType } from "@/external/caching/cacheActions.js"; -import { routeHandler } from "@/utils/routerUtils.js"; -import { encryptData } from "@/utils/encryptUtils.js"; -import Stripe from "stripe"; import { checkKeyValid, createWebhookEndpoint, } from "@/external/stripe/stripeOnboardingUtils.js"; +import { getSvixDashboardUrl } from "@/external/svix/svixHelpers.js"; +import { withOrgAuth } from "@/middleware/authMiddleware.js"; +import { encryptData } from "@/utils/encryptUtils.js"; +import { handleRequestError } from "@/utils/errorUtils.js"; +import { routeHandler } from "@/utils/routerUtils.js"; +import { OrgService } from "../orgs/OrgService.js"; import { clearOrgCache } from "../orgs/orgUtils/clearOrgCache.js"; -import * as crypto from "crypto"; import { isStripeConnected } from "../orgs/orgUtils.js"; +import { ApiKeyService } from "./ApiKeyService.js"; +import { createKey } from "./api-keys/apiKeyUtils.js"; export const devRouter: Router = Router(); @@ -82,7 +82,7 @@ devRouter.delete("/api_key/:id", withOrgAuth, async (req: any, res) => { const { db, orgId } = req; const { id } = req.params; - let data = await ApiKeyService.delete({ + const data = await ApiKeyService.delete({ db, id, orgId, @@ -94,8 +94,8 @@ devRouter.delete("/api_key/:id", withOrgAuth, async (req: any, res) => { return; } - let batchInvalidate = []; - for (let apiKey of data) { + const batchInvalidate = []; + for (const apiKey of data) { batchInvalidate.push( CacheManager.invalidate({ action: CacheType.SecretKey, @@ -240,14 +240,14 @@ export const handleGetOtp = async (req: any, res: any) => userId: req.user?.id, }); - let org = await OrgService.get({ + const org = await OrgService.get({ db: req.db, orgId: cacheData.orgId, }); - let stripeConnected = isStripeConnected({ org, env: AppEnv.Sandbox }); + const stripeConnected = isStripeConnected({ org, env: AppEnv.Sandbox }); - let responseData = { + const responseData = { ...cacheData, stripe_connected: stripeConnected, sandboxKey, @@ -265,9 +265,9 @@ export const handleGetOtp = async (req: any, res: any) => if (!stripeConnected) { // we need to generate a key for the CLI to use. - let key = generateRandomKey(); + const key = generateRandomKey(); responseData.stripeFlowAuthKey = key; - let stripeCacheData = { + const stripeCacheData = { orgId: cacheData.orgId, }; await CacheManager.setJson(key, stripeCacheData, OTP_TTL); @@ -283,7 +283,7 @@ devRouter.post("/cli/stripe", async (req: any, res: any) => { res, action: "Get Stripe Flow Auth Key", handler: async () => { - const { db, logtail: logger } = req; + const { db, logger } = req; const key = req.headers["authorization"]; if (!key) { res.status(401).json({ message: "Unauthorized" }); @@ -345,7 +345,7 @@ devRouter.post("/cli/stripe", async (req: any, res: any) => { }, }); - let redisClient = await CacheManager.getClient(); + const redisClient = await CacheManager.getClient(); if (!redisClient) { res.status(500).json({ message: "Cache client not initialized" }); return; diff --git a/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts b/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts index ef0ca2024..e6be7d793 100644 --- a/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts +++ b/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts @@ -112,7 +112,7 @@ export const handlePostEntityRequest = async (req: any, res: any) => res, action: "create entity", handler: async (req: any, res: any) => { - const { logtail: logger, org } = req; + const { logger, org } = req; const apiVersion = orgToVersion({ org, diff --git a/server/src/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.ts b/server/src/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.ts index f000afa20..28eee310a 100644 --- a/server/src/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.ts +++ b/server/src/internal/entities/handlers/handleDeleteEntity/cancelSubsForEntity.ts @@ -1,9 +1,13 @@ -import { createStripeCli } from "@/external/stripe/utils.js"; +import { + CusProductStatus, + type Entity, + type FullCusProduct, +} from "@autumn/shared"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { cancelCurSubs } from "@/internal/customers/change-product/handleDowngrade/cancelCurSubs.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { cusProductsToStripeSubs } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { CusProductStatus, Entity, FullCusProduct } from "@autumn/shared"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; export const cancelSubsForEntity = async ({ req, @@ -14,10 +18,10 @@ export const cancelSubsForEntity = async ({ cusProducts: FullCusProduct[]; entity: Entity; }) => { - const { org, env, db, logtail: logger } = req; + const { org, env, db, logger } = req; try { - let stripeCli = createStripeCli({ org, env }); - let curSubs = await cusProductsToStripeSubs({ + const stripeCli = createStripeCli({ org, env }); + const curSubs = await cusProductsToStripeSubs({ cusProducts, stripeCli, }); diff --git a/server/src/internal/features/handlers/handleCreateFeature.ts b/server/src/internal/features/handlers/handleCreateFeature.ts index 6bf64bebe..86fc5cc4b 100644 --- a/server/src/internal/features/handlers/handleCreateFeature.ts +++ b/server/src/internal/features/handlers/handleCreateFeature.ts @@ -11,7 +11,7 @@ export const handleCreateFeature = async (req: any, res: any) => { try { console.log("Trying to create feature"); const data = req.body; - const { db, orgId, env, logtail: logger } = req; + const { db, orgId, env, logger } = req; const parsedFeature = validateFeature(data); const feature: Feature = { diff --git a/server/src/internal/features/handlers/handleUpdateFeature.ts b/server/src/internal/features/handlers/handleUpdateFeature.ts index 9df22ce3b..66fed96bd 100644 --- a/server/src/internal/features/handlers/handleUpdateFeature.ts +++ b/server/src/internal/features/handlers/handleUpdateFeature.ts @@ -260,7 +260,7 @@ export const handleUpdateFeature = async ( handler: async (req: any, res: any) => { const featureId = req.params.feature_id; const data = req.body; - const { db, orgId, env, logtail: logger } = req; + const { db, orgId, env, logger } = req; // 1. Get feature by ID const features = await FeatureService.getFromReq(req); diff --git a/server/src/internal/mainRouter.ts b/server/src/internal/mainRouter.ts index be95ddffc..a702901c0 100644 --- a/server/src/internal/mainRouter.ts +++ b/server/src/internal/mainRouter.ts @@ -4,7 +4,7 @@ import { Autumn } from "autumn-js"; import { autumnHandler } from "autumn-js/express"; import { Router } from "express"; import rateLimit from "express-rate-limit"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { withAuth, withOrgAuth } from "../middleware/authMiddleware.js"; import { adminRouter } from "./admin/adminRouter.js"; import { withAdminAuth } from "./admin/withAdminAuth.js"; @@ -17,7 +17,7 @@ import { InvoiceService } from "./invoices/InvoiceService.js"; import { handlePostOrg } from "./orgs/handlers/handlePostOrg.js"; import { onboardingRouter } from "./orgs/onboarding/onboardingRouter.js"; import { orgRouter } from "./orgs/orgRouter.js"; -import { productRouter } from "./products/internalProductRouter.js"; +import { expressProductRouter } from "./products/internalProductRouter.js"; import { viewsRouter } from "./saved-views/savedViewsRouter.js"; import { userRouter } from "./users/userRouter.js"; @@ -33,7 +33,7 @@ mainRouter.use("/users", withAuth, userRouter); mainRouter.use("/onboarding", withOrgAuth, onboardingRouter); mainRouter.use("/organization", withOrgAuth, orgRouter); mainRouter.use("/features", withOrgAuth, internalFeatureRouter); -mainRouter.use("/products", withOrgAuth, productRouter); +mainRouter.use("/products", withOrgAuth, expressProductRouter); mainRouter.use("/dev", devRouter); mainRouter.use("/customers", withOrgAuth, cusRouter); mainRouter.use("/query", withOrgAuth, analyticsRouter); diff --git a/server/src/internal/migrations/migrationSteps/migrateCustomer.ts b/server/src/internal/migrations/migrationSteps/migrateCustomer.ts index a4ef1d3d1..deeef3b00 100644 --- a/server/src/internal/migrations/migrationSteps/migrateCustomer.ts +++ b/server/src/internal/migrations/migrationSteps/migrateCustomer.ts @@ -8,7 +8,7 @@ import type { } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { CusService } from "@/internal/customers/CusService.js"; import { deleteCusCache } from "@/internal/customers/cusCache/updateCachedCus.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; @@ -56,7 +56,6 @@ export const migrateCustomer = async ({ org, features, logger, - logtail: logger, timestamp: Date.now(), } as ExtendedRequest; diff --git a/server/src/internal/migrations/migrationSteps/migrateCustomers.ts b/server/src/internal/migrations/migrationSteps/migrateCustomers.ts index b6ec885b7..ad16417d6 100644 --- a/server/src/internal/migrations/migrationSteps/migrateCustomers.ts +++ b/server/src/internal/migrations/migrationSteps/migrateCustomers.ts @@ -10,8 +10,8 @@ import { MigrationJobStep, } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; import { MigrationService } from "../MigrationService.js"; import { migrateCustomer } from "./migrateCustomer.js"; diff --git a/server/src/internal/orgs/AuthService.ts b/server/src/internal/orgs/AuthService.ts index 0fddb7853..e69de29bb 100644 --- a/server/src/internal/orgs/AuthService.ts +++ b/server/src/internal/orgs/AuthService.ts @@ -1,34 +0,0 @@ -// import { db } from "@/db/initDrizzle.js"; -// import { OrgService } from "./OrgService.js"; -// import { DrizzleCli } from "@/db/initDrizzle.js"; -// import { member, organizations } from "@autumn/shared"; -// import { generateId } from "@/utils/genUtils.js"; - -// export class AuthService { -// static async createOrg({ -// db, -// name, -// slug, -// userId, -// }: { -// db: DrizzleCli; -// name: string; -// slug: string; -// userId: string; -// }) { -// // 1. Create org -// await db.insert(organizations).values({ -// id: generateId("org"), -// name, -// slug, -// createdAt: new Date(), -// }); - -// await db.insert(member).values({ -// id: generateId("mem"), -// organizationId: org.id, -// userId, -// createdAt: new Date(), -// }); -// } -// } diff --git a/server/src/internal/orgs/OrgService.ts b/server/src/internal/orgs/OrgService.ts index 95ce3df31..61491f055 100644 --- a/server/src/internal/orgs/OrgService.ts +++ b/server/src/internal/orgs/OrgService.ts @@ -11,9 +11,10 @@ import { organizations, user, } from "@autumn/shared"; -import { and, eq, sql } from "drizzle-orm"; +import { and, eq, or, sql } from "drizzle-orm"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import RecaseError from "@/utils/errorUtils.js"; +import { FeatureService } from "../features/FeatureService.js"; import { clearOrgCache } from "./orgUtils/clearOrgCache.js"; export class OrgService { @@ -175,9 +176,10 @@ export class OrgService { features: { where: eq(features.env, env), }, + master: true, }, })) as Organization & { - features: Feature[]; + features?: Feature[]; }; if (!result) { @@ -193,7 +195,8 @@ export class OrgService { } const org = structuredClone(result); - delete (org as any).features; + delete org.features; + return { org: { ...org, @@ -284,4 +287,159 @@ export class OrgService { return result; } + + static async getByAccountId({ + db, + accountId, + }: { + db: DrizzleCli; + accountId: string; + }) { + const result = await db.query.organizations.findFirst({ + where: or( + eq( + sql`${organizations.test_stripe_connect}->>'default_account_id'`, + accountId, + ), + eq(sql`${organizations.test_stripe_connect}->>'account_id'`, accountId), + eq(sql`${organizations.live_stripe_connect}->>'account_id'`, accountId), + ), + with: { + master: true, + }, + }); + + if (!result) { + throw new RecaseError({ + message: "Organization not found", + code: ErrCode.OrgNotFound, + statusCode: 404, + }); + } + + const defaultAccountId = result?.test_stripe_connect?.default_account_id; + const testAccountId = result?.test_stripe_connect?.account_id; + + const env = + defaultAccountId === accountId || testAccountId === accountId + ? AppEnv.Sandbox + : AppEnv.Live; + + const features = await FeatureService.list({ + db, + orgId: result?.id || "", + env, + }); + + return { + features, + org: { + ...(result as Organization), + config: OrgConfigSchema.parse(result.config || {}), + }, + env, + }; + } + + static async findByStripeAccountId({ + db, + accountId, + env, + }: { + db: DrizzleCli; + accountId: string; + env: AppEnv; + }): Promise { + const result = await db.query.organizations.findFirst({ + where: or( + eq(sql`${organizations.test_stripe_connect}->>'account_id'`, accountId), + eq(sql`${organizations.live_stripe_connect}->>'account_id'`, accountId), + ), + }); + + return result as Organization; + } + + /** + * Update Stripe Connect account ID for an organization + */ + static async updateStripeConnect({ + db, + orgId, + accountId, + env, + }: { + db: DrizzleCli; + orgId: string; + accountId: string; + env: AppEnv; + }): Promise { + const [org] = await db + .select() + .from(organizations) + .where(eq(organizations.id, orgId)) + .limit(1); + + if (!org) { + throw new RecaseError({ + message: "Organization not found", + code: ErrCode.OrgNotFound, + statusCode: 404, + }); + } + + if (env === AppEnv.Sandbox) { + const currentConnect = org.test_stripe_connect || {}; + await db + .update(organizations) + .set({ + test_stripe_connect: { + ...currentConnect, + account_id: accountId, + }, + }) + .where(eq(organizations.id, orgId)); + } else { + const currentConnect = org.live_stripe_connect || {}; + await db + .update(organizations) + .set({ + live_stripe_connect: { + ...currentConnect, + account_id: accountId, + }, + }) + .where(eq(organizations.id, orgId)); + } + + await clearOrgCache({ db, orgId }); + } + + static async updateConnectWebhookSecret({ + db, + orgId, + env, + secret, + }: { + db: DrizzleCli; + orgId: string; + env: AppEnv; + secret: string; + }) { + const prefix = env === AppEnv.Sandbox ? "test" : "live"; + const org = await OrgService.get({ db, orgId }); + console.info(`Updating connect webhook secret for ${env} org ${orgId}`); + console.info(`Secret: ${secret}`); + await db + .update(organizations) + .set({ + stripe_config: { + ...(org.stripe_config || {}), + [`${prefix}_connect_webhook_secret`]: secret, + }, + }) + .where(eq(organizations.id, orgId)); + + await clearOrgCache({ db, orgId }); + } } diff --git a/server/src/internal/orgs/handlers/handleConnectStripe.ts b/server/src/internal/orgs/handlers/handleConnectStripe_old.ts similarity index 74% rename from server/src/internal/orgs/handlers/handleConnectStripe.ts rename to server/src/internal/orgs/handlers/handleConnectStripe_old.ts index 5e3a5182d..d472b1cc1 100644 --- a/server/src/internal/orgs/handlers/handleConnectStripe.ts +++ b/server/src/internal/orgs/handlers/handleConnectStripe_old.ts @@ -1,18 +1,17 @@ import { AppEnv, ErrCode } from "@autumn/shared"; import Stripe from "stripe"; import { z } from "zod"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { ensureStripeProductsWithEnv } from "@/external/stripe/stripeEnsureUtils.js"; import { checkKeyValid, createWebhookEndpoint, } from "@/external/stripe/stripeOnboardingUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { encryptData } from "@/utils/encryptUtils.js"; import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; import { nullish } from "@/utils/genUtils.js"; import { routeHandler } from "@/utils/routerUtils.js"; import { OrgService } from "../OrgService.js"; -import { clearOrgCache } from "../orgUtils/clearOrgCache.js"; import { isStripeConnected } from "../orgUtils.js"; export const connectStripe = async ({ @@ -49,6 +48,10 @@ export const connectStripe = async ({ test_webhook_secret: encryptData(webhook.secret as string), env, defaultCurrency: account.default_currency, + metadata: { + org_id: orgId, + env: env, + }, }; } else { return { @@ -56,92 +59,14 @@ export const connectStripe = async ({ live_webhook_secret: encryptData(webhook.secret as string), env, defaultCurrency: account.default_currency, + metadata: { + org_id: orgId, + env: env, + }, }; } }; -export const connectAllStripe = async ({ - db, - orgId, - logger, - testApiKey, - liveApiKey, - defaultCurrency, - successUrl, -}: { - db: any; - orgId: string; - logger: any; - testApiKey: string; - liveApiKey: string; - defaultCurrency?: string; - successUrl: string; -}) => { - // 1. Check if API keys are valid - try { - await clearOrgCache({ - db, - orgId, - logger, - }); - - await checkKeyValid(testApiKey); - await checkKeyValid(liveApiKey); - - // Get default currency from Stripe - const stripe = new Stripe(testApiKey); - - const account = await stripe.accounts.retrieve(); - - if (nullish(defaultCurrency) && nullish(account.default_currency)) { - throw new RecaseError({ - message: "Default currency not set", - code: ErrCode.StripeKeyInvalid, - statusCode: 500, - }); - } else if (nullish(defaultCurrency)) { - defaultCurrency = account.default_currency; - } - } catch (error: any) { - // console.error("Error checking stripe keys", error); - throw new RecaseError({ - message: error.message || "Invalid Stripe API keys", - code: ErrCode.StripeKeyInvalid, - statusCode: 500, - data: error, - }); - } - // 2. Create webhook endpoint - let testWebhook: Stripe.WebhookEndpoint; - let liveWebhook: Stripe.WebhookEndpoint; - try { - testWebhook = await createWebhookEndpoint( - testApiKey, - AppEnv.Sandbox, - orgId, - ); - liveWebhook = await createWebhookEndpoint(liveApiKey, AppEnv.Live, orgId); - } catch (error) { - throw new RecaseError({ - message: "Error creating stripe webhook", - code: ErrCode.StripeKeyInvalid, - statusCode: 500, - data: error, - }); - } - - return { - defaultCurrency, - stripeConfig: { - test_api_key: encryptData(testApiKey), - live_api_key: encryptData(liveApiKey), - test_webhook_secret: encryptData(testWebhook.secret as string), - live_webhook_secret: encryptData(liveWebhook.secret as string), - success_url: successUrl, - }, - }; -}; - const connectStripeBody = z.object({ secret_key: z.string().optional(), success_url: z.string().optional(), @@ -298,9 +223,10 @@ export const handleGetStripe = async (req: any, res: any) => { } const stripeCli = createStripeCli({ org, env: req.env }); - const account_details = await stripeCli.accounts.retrieve(); + // console.log("Account details: ", account_details); + res.status(200).json(account_details); } catch (error) { handleRequestError({ req, error, res, action: "Get invoice" }); diff --git a/server/src/internal/orgs/handlers/handleDeleteOrg.ts b/server/src/internal/orgs/handlers/handleDeleteOrg.ts index 1fc973337..0093e04d5 100644 --- a/server/src/internal/orgs/handlers/handleDeleteOrg.ts +++ b/server/src/internal/orgs/handlers/handleDeleteOrg.ts @@ -1,9 +1,14 @@ +import { AppEnv, customers, ErrCode, type Organization } from "@autumn/shared"; +import { and, eq } from "drizzle-orm"; +import type { Response } from "express"; +import { + deauthorizeAccount, + deleteConnectedAccount, +} from "@/external/connect/connectUtils.js"; +import type { Logger } from "@/external/logtail/logtailUtils.js"; import { deleteSvixApp } from "@/external/svix/svixHelpers.js"; import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { AppEnv, customers, ErrCode, Organization } from "@autumn/shared"; -import { and, eq } from "drizzle-orm"; -import { Response } from "express"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; import { deleteStripeWebhook } from "../orgUtils.js"; const deleteSvixWebhooks = async ({ @@ -61,12 +66,44 @@ const deleteStripeWebhooks = async ({ } }; +export const deleteStripeAccounts = async ({ + org, + logger, +}: { + org: Organization; + logger: Logger; +}) => { + if (org.test_stripe_connect?.account_id) { + await deauthorizeAccount({ + accountId: org.test_stripe_connect.account_id, + env: AppEnv.Sandbox, + logger, + }); + } + + if (org.live_stripe_connect?.account_id) { + await deauthorizeAccount({ + accountId: org.live_stripe_connect.account_id, + env: AppEnv.Live, + logger, + }); + } + + if (org.test_stripe_connect?.default_account_id) { + await deleteConnectedAccount({ + accountId: org.test_stripe_connect.default_account_id, + env: AppEnv.Sandbox, + logger, + }); + } +}; + export const handleDeleteOrg = async (req: ExtendedRequest, res: Response) => { try { - const { org, db, logtail: logger } = req; + const { org, db, logger } = req; // 1. Check if any customers - let hasCustomers = await db.query.customers.findFirst({ + const hasCustomers = await db.query.customers.findFirst({ where: and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Live)), }); @@ -85,8 +122,12 @@ export const handleDeleteOrg = async (req: ExtendedRequest, res: Response) => { logger.info("2. Deleting stripe webhooks"); await deleteStripeWebhooks({ org, logger }); + // 4. Delete stripe accounts + logger.info("3. Deleting stripe accounts"); + await deleteStripeAccounts({ org, logger }); + // 4. Delete all sandbox customers - logger.info("3. Deleting sandbox customers"); + logger.info("4. Deleting sandbox customers"); await db .delete(customers) .where( diff --git a/server/src/internal/orgs/handlers/handleDeleteStripe.ts b/server/src/internal/orgs/handlers/handleDeleteStripe.ts deleted file mode 100644 index f14781d95..000000000 --- a/server/src/internal/orgs/handlers/handleDeleteStripe.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { routeHandler } from "@/utils/routerUtils.js"; -import { OrgService } from "../OrgService.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { clearOrgCache } from "../orgUtils/clearOrgCache.js"; -import { AppEnv, Organization } from "@autumn/shared"; -import { isStripeConnected } from "../orgUtils.js"; - -export const disconnectStripe = async ({ - org, - env, -}: { - org: Organization; - env: AppEnv; -}) => { - if (isStripeConnected({ org, env })) { - const stripeCli = createStripeCli({ org, env }); - const webhooks = await stripeCli.webhookEndpoints.list(); - for (const webhook of webhooks.data) { - if (webhook.url.includes(org.id) && webhook.url.includes(env)) { - await stripeCli.webhookEndpoints.del(webhook.id); - } - } - } -}; - -export const handleDeleteStripe = async (req: any, res: any) => - routeHandler({ - req, - res, - action: "delete stripe", - handler: async (req: any, res: any) => { - const org = await OrgService.getFromReq(req); - - let { db, orgId, logtail: logger } = req; - await clearOrgCache({ - db, - orgId, - logger, - }); - - try { - await disconnectStripe({ org, env: req.env }); - } catch (error) { - logger.error(`Failed to disconnect stripe for ${org.id}, ${org.slug}`, { - error, - }); - } - - // Update stripe config: - const newStripeConfig = structuredClone(req.org.stripe_config); - if (req.env === AppEnv.Sandbox) { - newStripeConfig.test_api_key = null; - } else { - newStripeConfig.live_api_key = null; - } - - await OrgService.update({ - db, - orgId: req.orgId, - updates: { - stripe_config: newStripeConfig, - }, - }); - - res.status(200).json({ - message: "Stripe disconnected", - }); - }, - }); diff --git a/server/src/internal/orgs/handlers/handleGetInvites.ts b/server/src/internal/orgs/handlers/handleGetInvites.ts index bdf9c5c4c..958b11a00 100644 --- a/server/src/internal/orgs/handlers/handleGetInvites.ts +++ b/server/src/internal/orgs/handlers/handleGetInvites.ts @@ -1,6 +1,9 @@ -import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js"; import { invitation, user as userTable } from "@autumn/shared"; import { and, eq, gt } from "drizzle-orm"; +import type { + ExtendedRequest, + ExtendedResponse, +} from "@/utils/models/Request.js"; export const handleGetInvites = async ( req: ExtendedRequest, diff --git a/server/src/internal/orgs/handlers/handleGetOrg.ts b/server/src/internal/orgs/handlers/handleGetOrg.ts index c2746bf3a..3334d73ae 100644 --- a/server/src/internal/orgs/handlers/handleGetOrg.ts +++ b/server/src/internal/orgs/handlers/handleGetOrg.ts @@ -1,7 +1,10 @@ -import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js"; +import type { + ExtendedRequest, + ExtendedResponse, +} from "@/utils/models/Request.js"; import { routeHandler } from "@/utils/routerUtils.js"; -import { createOrgResponse } from "../orgUtils.js"; import { OrgService } from "../OrgService.js"; +import { createOrgResponse } from "../orgUtils.js"; export const handleGetOrg = async (req: any, res: any) => routeHandler({ diff --git a/server/src/internal/orgs/handlers/handleGetOrgMembers.ts b/server/src/internal/orgs/handlers/handleGetOrgMembers.ts index b7c3c720b..2c737768e 100644 --- a/server/src/internal/orgs/handlers/handleGetOrgMembers.ts +++ b/server/src/internal/orgs/handlers/handleGetOrgMembers.ts @@ -1,8 +1,7 @@ +import { session as authSession, member } from "@autumn/shared"; +import { and, eq } from "drizzle-orm"; import { handleFrontendReqError } from "@/utils/errorUtils.js"; import { OrgService } from "../OrgService.js"; -import { auth } from "@/utils/auth.js"; -import { eq, and } from "drizzle-orm"; -import { member, session as authSession } from "@autumn/shared"; export const handleGetOrgMembers = async (req: any, res: any) => { try { @@ -77,7 +76,7 @@ export const handleRemoveMember = async (req: any, res: any) => { ); } catch (error) { // Log but don't fail the request if session revocation fails - req.logtail?.warn( + req.logger?.warn( `Failed to revoke sessions for user ${existingMember.userId} in org ${orgId}:`, error, ); diff --git a/server/src/internal/orgs/handlers/handleGetUploadUrl.ts b/server/src/internal/orgs/handlers/handleGetUploadUrl.ts index 73683f28c..cc96bfb54 100644 --- a/server/src/internal/orgs/handlers/handleGetUploadUrl.ts +++ b/server/src/internal/orgs/handlers/handleGetUploadUrl.ts @@ -1,13 +1,13 @@ +import { ErrCode } from "@autumn/shared"; import { logger } from "@/external/logtail/logtailUtils.js"; import { getUploadUrl } from "@/external/supabase/storageUtils.js"; import RecaseError, { handleFrontendReqError } from "@/utils/errorUtils.js"; -import { ErrCode } from "@autumn/shared"; export const handleGetUploadUrl = async (req: any, res: any) => { try { const { org } = req; - let path = `logo/${org.id}`; + const path = `logo/${org.id}`; if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) { logger.warn("Supabase storage not set up"); diff --git a/server/src/internal/orgs/handlers/handlePostOrg.ts b/server/src/internal/orgs/handlers/handlePostOrg.ts index 4518092ac..3a40725f2 100644 --- a/server/src/internal/orgs/handlers/handlePostOrg.ts +++ b/server/src/internal/orgs/handlers/handlePostOrg.ts @@ -1,4 +1,7 @@ -import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js"; +import type { + ExtendedRequest, + ExtendedResponse, +} from "@/utils/models/Request.js"; import { routeHandler } from "@/utils/routerUtils.js"; export const handlePostOrg = async (req: any, res: any) => diff --git a/server/src/internal/orgs/handlers/handleUpdateOrg.ts b/server/src/internal/orgs/handlers/handleUpdateOrg.ts index a78b77503..38fb657d0 100644 --- a/server/src/internal/orgs/handlers/handleUpdateOrg.ts +++ b/server/src/internal/orgs/handlers/handleUpdateOrg.ts @@ -1,23 +1,23 @@ -// import { auth } from "@/utils/auth.js"; -// import { handleFrontendReqError } from "@/utils/errorUtils.js"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { OrgService } from "../OrgService.js"; -// export const handleUpdateOrg = async (req: any, res: any) => { -// try { -// await auth.api.updateOrganization({ -// data: { -// name: req.body.name, -// slug: req.body.slug, -// }, -// organizationId: req.org.id, -// }); +export const handleUpdateOrg = createRoute({ + body: z.object({ + onboarded: z.boolean().optional(), + }), + handler: async (c) => { + const ctx = c.get("ctx"); + const { db, org } = ctx; -// res.status(200).json({ success: true }); -// } catch (error) { -// handleFrontendReqError({ -// req, -// error, -// res, -// action: "update org", -// }); -// } -// }; + const { onboarded } = c.req.valid("json"); + + await OrgService.update({ + db, + orgId: org.id, + updates: { onboarded }, + }); + + return c.json({ success: true }); + }, +}); diff --git a/server/src/internal/orgs/handlers/stripeHandlers/handleConnectStripe.ts b/server/src/internal/orgs/handlers/stripeHandlers/handleConnectStripe.ts new file mode 100644 index 000000000..3af5da5b8 --- /dev/null +++ b/server/src/internal/orgs/handlers/stripeHandlers/handleConnectStripe.ts @@ -0,0 +1,88 @@ +import { AppEnv, type StripeConfig } from "@autumn/shared"; +import { z } from "zod/v4"; +import { ensureStripeProductsWithEnv } from "@/external/stripe/stripeEnsureUtils.js"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import { OrgService } from "../../OrgService.js"; +import { handleStripeSecretKey } from "../../orgUtils/handleStripeSecretKey.js"; + +// Connecting stripe +const validateConnectStripeRequest = () => {}; + +const addSuccessUrlToUpdates = ({ + success_url, + env, + configUpdates, +}: { + success_url?: string; + env: AppEnv; + configUpdates: StripeConfig; +}) => { + if (success_url === undefined) return; + + if (env === AppEnv.Sandbox) { + configUpdates.sandbox_success_url = success_url; + } else { + configUpdates.success_url = success_url; + } +}; + +export const handleConnectStripe = createRoute({ + body: z.object({ + secret_key: z.string().optional(), + success_url: z.string().optional(), + default_currency: z.string().optional(), + }), + handler: async (c) => { + const ctx = c.get("ctx"); + const { db, org, logger, env } = ctx; + + const body = c.req.valid("json"); + const configUpdates: StripeConfig = org.stripe_config || {}; + + if (body.secret_key) { + const result = await handleStripeSecretKey({ + orgId: org.id, + secretKey: body.secret_key, + env, + }); + + if (env === AppEnv.Sandbox) { + configUpdates.test_api_key = result.test_api_key; + configUpdates.test_webhook_secret = result.test_webhook_secret; + } else { + configUpdates.live_api_key = result.live_api_key; + configUpdates.live_webhook_secret = result.live_webhook_secret; + } + } + + addSuccessUrlToUpdates({ + success_url: body.success_url, + env, + configUpdates, + }); + + const newOrg = await OrgService.update({ + db, + orgId: org.id, + updates: { + default_currency: body.default_currency, + stripe_config: configUpdates, + }, + }); + + if (newOrg) { + await ensureStripeProductsWithEnv({ + db, + logger, + req: ctx as ExtendedRequest, + org: newOrg, + env, + }); + } + + return c.json({ + message: "Connect Stripe", + }); + }, +}); diff --git a/server/src/internal/orgs/handlers/stripeHandlers/handleDeleteStripe.ts b/server/src/internal/orgs/handlers/stripeHandlers/handleDeleteStripe.ts new file mode 100644 index 000000000..a1de40d14 --- /dev/null +++ b/server/src/internal/orgs/handlers/stripeHandlers/handleDeleteStripe.ts @@ -0,0 +1,140 @@ +import { + AppEnv, + type Organization, + type StripeConnectConfig, +} from "@autumn/shared"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { orgToAccountId } from "@/external/connect/connectUtils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { initMasterStripe } from "@/external/connect/initStripeCli.js"; +import type { Logger } from "@/external/logtail/logtailUtils.js"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { OrgService } from "../../OrgService.js"; +import { clearOrgCache } from "../../orgUtils/clearOrgCache.js"; +import { isStripeConnected } from "../../orgUtils.js"; + +export const disconnectStripe = async ({ + org, + env, + logger, +}: { + org: Organization; + env: AppEnv; + logger: Logger; +}) => { + if (isStripeConnected({ org, env, throughSecretKey: true })) { + const stripeCli = createStripeCli({ org, env, throughSecretKey: true }); + const webhooks = await stripeCli.webhookEndpoints.list(); + for (const webhook of webhooks.data) { + if (webhook.url.includes(org.id) && webhook.url.includes(env)) { + await stripeCli.webhookEndpoints.del(webhook.id); + } + } + } + + const accountId = orgToAccountId({ org, env, noDefaultAccount: true }); + + if (accountId) { + const masterStripe = initMasterStripe({ env }); + + // OAuth-connected accounts must be deauthorized, not deleted + // Platform-managed accounts can be deleted + try { + await masterStripe.oauth.deauthorize({ + client_id: + env === AppEnv.Live + ? process.env.STRIPE_LIVE_CLIENT_ID || "" + : process.env.STRIPE_SANDBOX_CLIENT_ID || "", + stripe_user_id: accountId, + }); + } catch (error) { + // If deauthorization fails, the account might have already been disconnected + // or it's a platform-managed account that needs to be deleted + logger.error( + "Failed to deauthorize account, attempting deletion:", + error, + ); + } + } +}; + +export const clearStripeConfig = async ({ + db, + org, + env, +}: { + db: DrizzleCli; + org: Organization; + env: AppEnv; +}) => { + const newStripeConfig: any = structuredClone(org.stripe_config) || {}; + + if (env === AppEnv.Sandbox) { + newStripeConfig.test_api_key = null; + } else { + newStripeConfig.live_api_key = null; + } + + await OrgService.update({ + db, + orgId: org.id, + updates: { + stripe_config: newStripeConfig, + }, + }); +}; + +export const handleDeleteStripe = createRoute({ + handler: async (c) => { + const ctx = c.get("ctx"); + const { db, org, logger, env } = ctx; + + await clearOrgCache({ + db, + orgId: org.id, + logger, + }); + + try { + await disconnectStripe({ org, env, logger }); + } catch (error) { + logger.error(`Failed to disconnect stripe for ${org.id}, ${org.slug}`, { + error, + }); + } + + // Update stripe config: + + if (isStripeConnected({ org, env, throughSecretKey: true })) { + await clearStripeConfig({ db, org, env }); + } else if (orgToAccountId({ org, env, noDefaultAccount: true })) { + if (env === AppEnv.Sandbox) { + const newStripeConnect: StripeConnectConfig = + structuredClone(org.test_stripe_connect) || {}; + delete newStripeConnect.account_id; + + await OrgService.update({ + db, + orgId: org.id, + updates: { + test_stripe_connect: newStripeConnect, + }, + }); + } else { + const newStripeConnect: StripeConnectConfig = + structuredClone(org.live_stripe_connect) || {}; + delete newStripeConnect.account_id; + + await OrgService.update({ + db, + orgId: org.id, + updates: { + live_stripe_connect: newStripeConnect, + }, + }); + } + } + + return c.json({}); + }, +}); diff --git a/server/src/internal/orgs/handlers/stripeHandlers/handleGetDashboardUrl.ts b/server/src/internal/orgs/handlers/stripeHandlers/handleGetDashboardUrl.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.ts b/server/src/internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.ts new file mode 100644 index 000000000..5a66f69ec --- /dev/null +++ b/server/src/internal/orgs/handlers/stripeHandlers/handleGetOAuthUrl.ts @@ -0,0 +1,55 @@ +import { AppEnv, ErrCode, RecaseError } from "@autumn/shared"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { generateOAuthState } from "@/internal/platform/platformBeta/utils/oauthStateUtils.js"; + +export const handleGetOAuthUrl = createRoute({ + handler: async (c) => { + const ctx = c.get("ctx"); + const { org, env } = ctx; + + const clientId = + env === AppEnv.Live + ? process.env.STRIPE_LIVE_CLIENT_ID + : process.env.STRIPE_SANDBOX_CLIENT_ID; + + if (!clientId) { + throw new RecaseError({ + message: `Stripe ${env === AppEnv.Live ? "live" : "test"} client ID not configured`, + code: ErrCode.InternalError, + statusCode: 500, + }); + } + + // Generate OAuth state and store in Redis + const frontendUrl = process.env.CLIENT_URL || "http://localhost:5173"; + + const redirectUri = `${frontendUrl}/dev?tab=stripe`; + + const stateKey = await generateOAuthState({ + organizationSlug: org.slug, + env: env === AppEnv.Live ? "live" : "test", + redirectUri, + masterOrgId: null, // null for standard flow + }); + + const baseUrl = new URL( + `https://connect.stripe.com/oauth/v2/authorize?response_type=code&client_id=${clientId}&scope=read_write`, + ); + + let serverUrl = process.env.BETTER_AUTH_URL; + if (env === AppEnv.Live && serverUrl?.includes("localhost")) { + serverUrl = `https://express.dev.useautumn.com`; + } + + // Add state + redirect_uri + baseUrl.searchParams.set("state", stateKey); + baseUrl.searchParams.set( + "redirect_uri", + `${serverUrl}/stripe/oauth_callback`, + ); + + return c.json({ + oauth_url: baseUrl.toString(), + }); + }, +}); diff --git a/server/src/internal/orgs/handlers/stripeHandlers/handleGetStripeAccount.ts b/server/src/internal/orgs/handlers/stripeHandlers/handleGetStripeAccount.ts new file mode 100644 index 000000000..1c9630769 --- /dev/null +++ b/server/src/internal/orgs/handlers/stripeHandlers/handleGetStripeAccount.ts @@ -0,0 +1,19 @@ +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { isStripeConnected } from "../../orgUtils.js"; + +export const handleGetStripeAccount = createRoute({ + handler: async (c) => { + const ctx = c.get("ctx"); + const { org, env } = ctx; + + if (!isStripeConnected({ org, env })) { + return c.json(null); + } + + const stripeCli = createStripeCli({ org, env }); + const account_details = await stripeCli.accounts.retrieve(); + + return c.json(account_details); + }, +}); diff --git a/server/src/internal/orgs/handlers/stripeHandlers/handleOAuthCallback.ts b/server/src/internal/orgs/handlers/stripeHandlers/handleOAuthCallback.ts new file mode 100644 index 000000000..fd55f9603 --- /dev/null +++ b/server/src/internal/orgs/handlers/stripeHandlers/handleOAuthCallback.ts @@ -0,0 +1,145 @@ +import { AppEnv } from "@autumn/shared"; +import type { Context } from "hono"; +import { initDrizzle } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { initMasterStripe } from "@/external/connect/initStripeCli.js"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { consumeOAuthState } from "@/internal/platform/platformBeta/utils/oauthStateUtils.js"; + +/** + * Handles Stripe OAuth callback + * Uses Redis state for both standard and platform flows + */ +export const handleOAuthCallback = async (c: Context) => { + const query = c.req.query(); + const { code, state, error } = query; + + // Get database connection + const { db } = initDrizzle(); + + // Build frontend redirect URL (default) + const frontendUrl = process.env.CLIENT_URL || "http://localhost:5173"; + let redirectUrl = new URL(`${frontendUrl}`); + redirectUrl.searchParams.set("tab", "stripe"); + + // Handle OAuth error from Stripe + if (error) { + redirectUrl.searchParams.set("error", error); + return c.redirect(redirectUrl.toString()); + } + + // Validate required parameters + if (!code || !state) { + redirectUrl.searchParams.set("error", "missing_parameters"); + return c.redirect(redirectUrl.toString()); + } + + try { + // Consume OAuth state from Redis + const redisState = await consumeOAuthState({ stateKey: state }); + + if (!redisState) { + redirectUrl.searchParams.set("error", "invalid_state"); + return c.redirect(redirectUrl.toString()); + } + + // Extract state data + const { + organization_slug, + env: envStr, + redirect_uri, + master_org_id, + } = redisState; + const env = envStr === "live" ? AppEnv.Live : AppEnv.Sandbox; + const isPlatformFlow = master_org_id !== null; + + // Use custom redirect URI if provided (platform flow) + if (isPlatformFlow) { + redirectUrl = new URL(redirect_uri); + } else { + redirectUrl = new URL( + `${frontendUrl}${env === AppEnv.Sandbox ? "/sandbox" : ""}/dev?tab=stripe`, + ); + } + + // Fetch the organization by slug + const org = await OrgService.getBySlug({ db, slug: organization_slug }); + + if (!org) { + console.error("Organization not found:", organization_slug); + redirectUrl.searchParams.set("error", "org_not_found"); + return c.redirect(redirectUrl.toString()); + } + + const stripe = initMasterStripe({ env }); + const response = await stripe.oauth.token({ + grant_type: "authorization_code", + code, + }); + + const accountId = response.stripe_user_id; + + if (!accountId) { + console.error("Account ID not found"); + redirectUrl.searchParams.set("error", "account_id_not_found"); + return c.redirect(redirectUrl.toString()); + } + + // Check if account ID is already connected to another organization + const existingOrg = await OrgService.findByStripeAccountId({ + db, + accountId, + env, + }); + + if (existingOrg) { + console.error( + `Account ${accountId} is already connected to org ${existingOrg.id}`, + ); + + // Platform flow just returns error code + if (isPlatformFlow) { + redirectUrl.searchParams.set("error", "account_already_connected"); + return c.redirect(redirectUrl.toString()); + } + + // Standard flow returns detailed error + const master = createStripeCli({ org: existingOrg, env }); + const account = await master.accounts.retrieve(accountId); + redirectUrl.searchParams.set("error", "account_already_connected"); + redirectUrl.searchParams.set("account_id", accountId); + redirectUrl.searchParams.set("account_name", account.company?.name || ""); + redirectUrl.searchParams.set( + "connected_org_name", + existingOrg.name || "", + ); + redirectUrl.searchParams.set( + "connected_org_slug", + existingOrg.slug || "", + ); + return c.redirect(redirectUrl.toString()); + } + + // Update organization with Stripe Connect account + await OrgService.updateStripeConnect({ + db, + orgId: org.id, + accountId, + env, + }); + + console.log(`Successfully connected Stripe account for org ${org.id}`); + + // Redirect to success + redirectUrl.searchParams.set("success", "true"); + return c.redirect(redirectUrl.toString()); + } catch (error: unknown) { + console.error("Error in OAuth callback:", error); + redirectUrl.searchParams.set( + "error", + error instanceof Error ? error.message : "unknown_error", + ); + return c.redirect(redirectUrl.toString()); + } +}; diff --git a/server/src/internal/orgs/onboarding/createOnboardingProducts.ts b/server/src/internal/orgs/onboarding/createOnboardingProducts.ts index 4a7a3be12..34de26265 100644 --- a/server/src/internal/orgs/onboarding/createOnboardingProducts.ts +++ b/server/src/internal/orgs/onboarding/createOnboardingProducts.ts @@ -1,21 +1,20 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { generateId, keyToTitle } from "@/utils/genUtils.js"; import { - FeatureType, AggregateType, - FeatureUsageType, - EntInterval, AllowanceType, - PriceType, BillingInterval, + EntInterval, // DB Models entitlements, - prices, + FeatureType, + FeatureUsageType, features, + PriceType, + prices, products, } from "@autumn/shared"; - import { AppEnv } from "autumn-js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { generateId, keyToTitle } from "@/utils/genUtils.js"; const defaultFeatures = [ { @@ -119,7 +118,7 @@ export const createOnboardingProducts = async ({ const batchInsert = []; for (const product of defaultProducts) { const insertProduct = async (product: any) => { - let internalProductId = generateId("pr"); + const internalProductId = generateId("pr"); await db.insert(products).values({ ...product, diff --git a/server/src/internal/orgs/onboarding/onboardingRouter.ts b/server/src/internal/orgs/onboarding/onboardingRouter.ts index 931350a80..0c376c957 100644 --- a/server/src/internal/orgs/onboarding/onboardingRouter.ts +++ b/server/src/internal/orgs/onboarding/onboardingRouter.ts @@ -1,17 +1,18 @@ -import { Router } from "express"; - +import { AppEnv, chatResults } from "@autumn/shared"; import { eq } from "drizzle-orm"; -import { routeHandler } from "@/utils/routerUtils.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { AppEnv } from "@autumn/shared"; -import { parseChatResultFeatures } from "./parseChatFeatures.js"; -import { parseChatProducts } from "./parseChatProducts.js"; -import { chatResults } from "@autumn/shared"; -import { ProductService } from "@/internal/products/ProductService.js"; +import { Router } from "express"; import { FeatureService } from "@/internal/features/FeatureService.js"; import { EntitlementService } from "@/internal/products/entitlements/EntitlementService.js"; +import { ProductService } from "@/internal/products/ProductService.js"; import { PriceService } from "@/internal/products/prices/PriceService.js"; -import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js"; +import RecaseError from "@/utils/errorUtils.js"; +import type { + ExtendedRequest, + ExtendedResponse, +} from "@/utils/models/Request.js"; +import { routeHandler } from "@/utils/routerUtils.js"; +import { parseChatResultFeatures } from "./parseChatFeatures.js"; +import { parseChatProducts } from "./parseChatProducts.js"; export const onboardingRouter: Router = Router(); @@ -21,7 +22,7 @@ onboardingRouter.post("", async (req: Request, res: any) => res, action: "onboarding", handler: async (req: ExtendedRequest, res: ExtendedResponse) => { - const { db, logtail: logger, org } = req; + const { db, logger, org } = req; const { token } = req.body; if (!token) { @@ -32,7 +33,7 @@ onboardingRouter.post("", async (req: Request, res: any) => }); } - let chatResult = await db.query.chatResults.findFirst({ + const chatResult = await db.query.chatResults.findFirst({ where: eq(chatResults.id, token), }); @@ -44,33 +45,33 @@ onboardingRouter.post("", async (req: Request, res: any) => }); } - let curProducts = await ProductService.listFull({ + const curProducts = await ProductService.listFull({ db, orgId: org.id, env: AppEnv.Sandbox, }); - let curFeatures = await FeatureService.list({ + const curFeatures = await FeatureService.list({ db, orgId: org.id, env: AppEnv.Sandbox, }); - let newProducts = chatResult.data.products.filter((product) => { + const newProducts = chatResult.data.products.filter((product) => { return !curProducts.some((p) => p.id === product.id); }); - let newFeatures = chatResult.data.features.filter((feature) => { + const newFeatures = chatResult.data.features.filter((feature) => { return !curFeatures.some((f) => f.id === feature.id); }); if (newFeatures.length > 0 || newProducts.length > 0) { - let backendFeatures = parseChatResultFeatures({ + const backendFeatures = parseChatResultFeatures({ features: newFeatures, orgId: org.id, }); - let { products, prices, ents } = await parseChatProducts({ + const { products, prices, ents } = await parseChatProducts({ db, logger, orgId: org.id, diff --git a/server/src/internal/orgs/onboarding/parseChatFeatures.ts b/server/src/internal/orgs/onboarding/parseChatFeatures.ts index 6a0259055..9acbf0ac4 100644 --- a/server/src/internal/orgs/onboarding/parseChatFeatures.ts +++ b/server/src/internal/orgs/onboarding/parseChatFeatures.ts @@ -1,17 +1,17 @@ -import { validateMeteredConfig } from "@/internal/features/featureUtils.js"; -import { constructFeature } from "@/internal/features/utils/constructFeatureUtils.js"; -import RecaseError from "@/utils/errorUtils.js"; -import { keyToTitle } from "@/utils/genUtils.js"; import { AggregateType, AppEnv, ChatFeatureCreditSchema, - ChatResultFeature, + type ChatResultFeature, + type CreditSystemConfig, FeatureType, FeatureUsageType, - MeteredConfig, + type MeteredConfig, } from "@autumn/shared"; -import { CreditSystemConfig } from "@autumn/shared"; +import { validateMeteredConfig } from "@/internal/features/featureUtils.js"; +import { constructFeature } from "@/internal/features/utils/constructFeatureUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { keyToTitle } from "@/utils/genUtils.js"; const validateFeatures = (features: ChatResultFeature[]) => { features.forEach((feature) => { @@ -32,7 +32,7 @@ const validateFeatures = (features: ChatResultFeature[]) => { }); } - let meteredFeature = features.some( + const meteredFeature = features.some( (m) => m.id == item.metered_feature_id && m.id != feature.id, ); if (!meteredFeature) { @@ -58,14 +58,14 @@ export const parseChatResultFeatures = ({ validateFeatures(features); return features.map((feature) => { - let type = + const type = feature.type == "boolean" ? FeatureType.Boolean : feature.type == "credit_system" ? FeatureType.CreditSystem : FeatureType.Metered; - let config: CreditSystemConfig | MeteredConfig | undefined = undefined; + let config: CreditSystemConfig | MeteredConfig | undefined; if (type == FeatureType.CreditSystem) { config = { schema: feature.credit_schema!.map((item) => ({ @@ -89,7 +89,7 @@ export const parseChatResultFeatures = ({ }); } - let backendFeat = constructFeature({ + const backendFeat = constructFeature({ id: feature.id, name: keyToTitle(feature.id), type, diff --git a/server/src/internal/orgs/orgRouter.ts b/server/src/internal/orgs/orgRouter.ts index f9f478f16..41fa31a30 100644 --- a/server/src/internal/orgs/orgRouter.ts +++ b/server/src/internal/orgs/orgRouter.ts @@ -1,10 +1,7 @@ import express, { type Router } from "express"; -import { - handleConnectStripe, - handleGetStripe, -} from "./handlers/handleConnectStripe.js"; +import { Hono } from "hono"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; import { handleDeleteOrg } from "./handlers/handleDeleteOrg.js"; -import { handleDeleteStripe } from "./handlers/handleDeleteStripe.js"; import { handleGetInvites } from "./handlers/handleGetInvites.js"; import { handleGetOrg } from "./handlers/handleGetOrg.js"; import { @@ -12,6 +9,11 @@ import { handleRemoveMember, } from "./handlers/handleGetOrgMembers.js"; import { handleGetUploadUrl } from "./handlers/handleGetUploadUrl.js"; +import { handleUpdateOrg } from "./handlers/handleUpdateOrg.js"; +import { handleConnectStripe } from "./handlers/stripeHandlers/handleConnectStripe.js"; +import { handleDeleteStripe } from "./handlers/stripeHandlers/handleDeleteStripe.js"; +import { handleGetOAuthUrl } from "./handlers/stripeHandlers/handleGetOAuthUrl.js"; +import { handleGetStripeAccount } from "./handlers/stripeHandlers/handleGetStripeAccount.js"; export const orgRouter: Router = express.Router(); orgRouter.get("/members", handleGetOrgMembers); @@ -28,115 +30,12 @@ orgRouter.delete("/delete-user", async (req: any, res) => { orgRouter.get("", handleGetOrg); -orgRouter.get("/stripe", handleGetStripe); +// orgRouter.post("/stripe", handleConnectStripe); -orgRouter.post("/stripe", handleConnectStripe); +export const honoOrgRouter = new Hono(); -orgRouter.delete("/stripe", handleDeleteStripe); - -// async (req: any, res) => { -// try { -// let { testApiKey, liveApiKey, successUrl, defaultCurrency } = req.body; -// let { db, orgId, logtail: logger } = req; -// if (!testApiKey || !liveApiKey || !successUrl) { -// throw new RecaseError({ -// message: "Missing required fields", -// code: ErrCode.StripeKeyInvalid, -// statusCode: 400, -// }); -// } - -// // 1. Check if API keys are valid -// try { -// await clearOrgCache({ -// db, -// orgId, -// logger, -// }); - -// console.log("Connecting Stripe"); -// await checkKeyValid(testApiKey); -// await checkKeyValid(liveApiKey); - -// // Get default currency from Stripe -// let stripe = new Stripe(testApiKey); -// let account = await stripe.accounts.retrieve(); - -// if (nullish(defaultCurrency) && nullish(account.default_currency)) { -// throw new RecaseError({ -// message: "Default currency not set", -// code: ErrCode.StripeKeyInvalid, -// statusCode: 500, -// }); -// } else if (nullish(defaultCurrency)) { -// defaultCurrency = account.default_currency; -// } -// } catch (error: any) { -// throw new RecaseError({ -// message: error.message || "Invalid Stripe API keys", -// code: ErrCode.StripeKeyInvalid, -// statusCode: 500, -// data: error, -// }); -// } - -// // 2. Create webhook endpoint -// let testWebhook: Stripe.WebhookEndpoint; -// let liveWebhook: Stripe.WebhookEndpoint; -// try { -// testWebhook = await createWebhookEndpoint( -// testApiKey, -// AppEnv.Sandbox, -// req.orgId -// ); -// liveWebhook = await createWebhookEndpoint( -// liveApiKey, -// AppEnv.Live, -// req.orgId -// ); -// } catch (error) { -// throw new RecaseError({ -// message: "Error creating stripe webhook", -// code: ErrCode.StripeKeyInvalid, -// statusCode: 500, -// data: error, -// }); -// } - -// // 1. Update org in Supabase first -// const updatedOrg = await OrgService.update({ -// db, -// orgId: req.orgId, -// updates: { -// stripe_connected: true, -// default_currency: defaultCurrency, -// stripe_config: { -// test_api_key: encryptData(testApiKey), -// live_api_key: encryptData(liveApiKey), -// test_webhook_secret: encryptData(testWebhook.secret as string), -// live_webhook_secret: encryptData(liveWebhook.secret as string), -// success_url: successUrl, -// }, -// }, -// }); - -// // 2. Ensure products are created in Stripe (after org is updated) -// await ensureStripeProducts({ -// db, -// logger, -// req, -// org: updatedOrg as Organization, -// }); - -// res.status(200).json({ -// message: "Stripe connected", -// }); -// } catch (error: any) { -// handleRequestError({ -// req, -// error, -// res, -// action: "connect stripe", -// }); -// } -// } +honoOrgRouter.patch("", ...handleUpdateOrg); +honoOrgRouter.get("/stripe", ...handleGetStripeAccount); +honoOrgRouter.delete("/stripe", ...handleDeleteStripe); +honoOrgRouter.post("/stripe", ...handleConnectStripe); +honoOrgRouter.get("/stripe/oauth_url", ...handleGetOAuthUrl); diff --git a/server/src/internal/orgs/orgUtils.ts b/server/src/internal/orgs/orgUtils.ts index 797b830ba..4917cd68c 100644 --- a/server/src/internal/orgs/orgUtils.ts +++ b/server/src/internal/orgs/orgUtils.ts @@ -1,23 +1,27 @@ -import { decryptData, generatePublishableKey } from "@/utils/encryptUtils.js"; -import RecaseError from "@/utils/errorUtils.js"; import { AppEnv, ErrCode, - FrontendOrg, - Organization, + type FrontendOrg, + type Organization, + type OrgConfig, organizations, - OrgConfig, } from "@autumn/shared"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { OrgService } from "./OrgService.js"; -import { FeatureService } from "../features/FeatureService.js"; -import { notNullish } from "@/utils/genUtils.js"; -import Stripe from "stripe"; -import { toSuccessUrl } from "./orgUtils/convertOrgUtils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { CacheManager } from "@/external/caching/CacheManager.js"; import { eq } from "drizzle-orm"; +import Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { CacheManager } from "@/external/caching/CacheManager.js"; +import { + orgToAccountId, + shouldUseMaster, +} from "@/external/connect/connectUtils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { decryptData, generatePublishableKey } from "@/utils/encryptUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; +import { notNullish } from "@/utils/genUtils.js"; +import { FeatureService } from "../features/FeatureService.js"; +import { OrgService } from "./OrgService.js"; import { clearOrgCache } from "./orgUtils/clearOrgCache.js"; +import { toSuccessUrl } from "./orgUtils/convertOrgUtils.js"; export const shouldReconnectStripe = async ({ org, @@ -33,7 +37,7 @@ export const shouldReconnectStripe = async ({ if (!isStripeConnected({ org, env })) return true; try { - const stripeCli = createStripeCli({ org, env: env! }); + const stripeCli = createStripeCli({ org, env }); const newKey = new Stripe(stripeKey); const oldAccount = await stripeCli.accounts.retrieve(); @@ -49,14 +53,52 @@ export const shouldReconnectStripe = async ({ export const isStripeConnected = ({ org, env, + throughSecretKey = false, + throughAccountId = false, + excludeDefault = false, }: { org: Organization; env?: AppEnv; + throughSecretKey?: boolean; + throughAccountId?: boolean; + excludeDefault?: boolean; }) => { + const testAccountId = orgToAccountId({ + org, + env: AppEnv.Sandbox, + noDefaultAccount: excludeDefault, + }); + + const liveAccountId = orgToAccountId({ + org, + env: AppEnv.Live, + noDefaultAccount: excludeDefault, + }); + if (env === AppEnv.Sandbox) { - return notNullish(org.stripe_config?.test_api_key); + if (throughAccountId) { + return notNullish(testAccountId); + } + + if (throughSecretKey) { + return notNullish(org.stripe_config?.test_api_key); + } + + return ( + notNullish(org.stripe_config?.test_api_key) || notNullish(testAccountId) + ); } else if (env === AppEnv.Live) { - return notNullish(org.stripe_config?.live_api_key); + if (throughAccountId) { + return notNullish(liveAccountId); + } + + if (throughSecretKey) { + return notNullish(org.stripe_config?.live_api_key); + } + + return ( + notNullish(org.stripe_config?.live_api_key) || notNullish(liveAccountId) + ); } else { return ( notNullish(org.stripe_config?.test_api_key) && @@ -90,9 +132,9 @@ export const deleteStripeWebhook = async ({ org: Organization; env: AppEnv; }) => { - if (!isStripeConnected({ org, env })) return; + if (!isStripeConnected({ org, env, throughSecretKey: true })) return; - const stripeCli = createStripeCli({ org, env }); + const stripeCli = createStripeCli({ org, env, throughSecretKey: true }); const webhookEndpoints = await stripeCli.webhookEndpoints.list({ limit: 100, }); @@ -146,11 +188,32 @@ export const createOrgResponse = ({ org: Organization; env: AppEnv; }): FrontendOrg => { + const accountId = orgToAccountId({ org, env, noDefaultAccount: true }); + const secretKeyConnected = isStripeConnected({ + org, + env, + throughSecretKey: true, + }); + + const stripeConnection = secretKeyConnected + ? "secret_key" + : accountId + ? "oauth" + : "default"; + + const throughMaster = shouldUseMaster({ org, env }); return { id: org.id, name: org.name, logo: org.logo, slug: org.slug, + master: org.master + ? { + id: org.master.id, + name: org.master.name, + slug: org.master.slug, + } + : null, // sandbox_config: { // stripe_connected: isStripeConnected({ org, env: AppEnv.Sandbox }), // default_currency: org.default_currency || "USD", @@ -164,17 +227,20 @@ export const createOrgResponse = ({ success_url: toSuccessUrl({ org, env }) || "", default_currency: org.default_currency || "usd", - stripe_connected: isStripeConnected({ org, env }), + stripe_connection: stripeConnection, + through_master: throughMaster, + created_at: new Date(org.createdAt).getTime(), test_pkey: org.test_pkey, live_pkey: org.live_pkey, + onboarded: org.onboarded ?? true, }; }; export const getOrgAndFeatures = async ({ req }: { req: any }) => { - let { orgId, env } = req; + const { orgId, env } = req; - let [org, features] = await Promise.all([ + const [org, features] = await Promise.all([ OrgService.getFromReq(req), FeatureService.getFromReq(req), ]); diff --git a/server/src/internal/orgs/orgUtils/clearOrgCache.ts b/server/src/internal/orgs/orgUtils/clearOrgCache.ts index aa8eb6491..2471be677 100644 --- a/server/src/internal/orgs/orgUtils/clearOrgCache.ts +++ b/server/src/internal/orgs/orgUtils/clearOrgCache.ts @@ -1,8 +1,8 @@ -import { AppEnv } from "@autumn/shared"; -import { OrgService } from "../OrgService.js"; +import type { AppEnv } from "@autumn/shared"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { CacheManager } from "@/external/caching/CacheManager.js"; import { CacheType } from "@/external/caching/cacheActions.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { OrgService } from "../OrgService.js"; export const clearOrgCache = async ({ db, @@ -17,7 +17,7 @@ export const clearOrgCache = async ({ }) => { // 1. Get all hashed secret key and public key for org try { - let org = await OrgService.getWithKeys({ + const org = await OrgService.getWithKeys({ db, orgId, env, @@ -27,11 +27,11 @@ export const clearOrgCache = async ({ return; } - let secretKeys = org.api_keys.map((key: any) => key.hashed_key); - let publicKeys = [org.test_pkey, org.live_pkey]; + const secretKeys = org.api_keys.map((key: any) => key.hashed_key); + const publicKeys = [org.test_pkey, org.live_pkey]; - let batchDelete = []; - for (let key of secretKeys) { + const batchDelete = []; + for (const key of secretKeys) { batchDelete.push( CacheManager.invalidate({ action: CacheType.SecretKey, @@ -40,7 +40,7 @@ export const clearOrgCache = async ({ ); } - for (let key of publicKeys) { + for (const key of publicKeys) { batchDelete.push( CacheManager.invalidate({ action: CacheType.PublicKey, diff --git a/server/src/internal/orgs/orgUtils/convertOrgUtils.ts b/server/src/internal/orgs/orgUtils/convertOrgUtils.ts index 025a5cfe8..e9c735a9d 100644 --- a/server/src/internal/orgs/orgUtils/convertOrgUtils.ts +++ b/server/src/internal/orgs/orgUtils/convertOrgUtils.ts @@ -1,4 +1,4 @@ -import { AppEnv, Organization } from "@autumn/shared"; +import { AppEnv, type Organization } from "@autumn/shared"; export const toSuccessUrl = ({ org, diff --git a/server/src/internal/orgs/orgUtils/createConnectAccount.ts b/server/src/internal/orgs/orgUtils/createConnectAccount.ts new file mode 100644 index 000000000..89cd72c4a --- /dev/null +++ b/server/src/internal/orgs/orgUtils/createConnectAccount.ts @@ -0,0 +1,44 @@ +import "dotenv/config"; +import { AppEnv } from "@autumn/shared"; +import type { User } from "better-auth"; +import type { Organization } from "better-auth/plugins"; +import { initMasterStripe } from "@/external/connect/initStripeCli.js"; + +export const createConnectAccount = async ({ + org, + user, +}: { + org: Organization; + user: User; +}) => { + // For v2 API, need to use specific API version + const stripe = initMasterStripe({ + env: AppEnv.Sandbox, + legacyVersion: false, // Ensure using latest API version + }); + + console.log("Creating connect account for org:", org.name); + + // Stripe v2 API for connected accounts + const account = await stripe.v2.core.accounts.create({ + contact_email: user.email, + display_name: org.name, + dashboard: "full", + identity: { + country: "us", + }, + configuration: { + merchant: {}, + }, + defaults: { + responsibilities: { + losses_collector: "stripe", + fees_collector: "stripe", + }, + }, + }); + + console.log("Created connected account:", account.id); + + return account; +}; diff --git a/server/src/internal/orgs/orgUtils/handleStripeSecretKey.ts b/server/src/internal/orgs/orgUtils/handleStripeSecretKey.ts new file mode 100644 index 000000000..77a3b9c94 --- /dev/null +++ b/server/src/internal/orgs/orgUtils/handleStripeSecretKey.ts @@ -0,0 +1,58 @@ +import { AppEnv } from "@autumn/shared"; +import Stripe from "stripe"; +import { + checkKeyValid, + createWebhookEndpoint, +} from "@/external/stripe/stripeOnboardingUtils.js"; +import { encryptData } from "@/utils/encryptUtils.js"; + +export const handleStripeSecretKey = async ({ + orgId, + secretKey, + env, +}: { + orgId: string; + secretKey: string; + env: AppEnv; +}) => { + // 1. Check if key is valid + await checkKeyValid(secretKey); + const stripe = new Stripe(secretKey); + const account = await stripe.accounts.retrieve(); + + // 2. Disconnect existing webhook endpoints + const curWebhooks = await stripe.webhookEndpoints.list(); + for (const webhook of curWebhooks.data) { + if (webhook.url.includes(orgId) && webhook.url.includes(env)) { + await stripe.webhookEndpoints.del(webhook.id); + } + } + + // 3. Create new webhook endpoint + const webhook = await createWebhookEndpoint(secretKey, env, orgId); + + // 3. Return encrypted + if (env === AppEnv.Sandbox) { + return { + test_api_key: encryptData(secretKey), + test_webhook_secret: encryptData(webhook.secret as string), + env, + defaultCurrency: account.default_currency, + metadata: { + org_id: orgId, + env: env, + }, + }; + } else { + return { + live_api_key: encryptData(secretKey), + live_webhook_secret: encryptData(webhook.secret as string), + env, + defaultCurrency: account.default_currency, + metadata: { + org_id: orgId, + env: env, + }, + }; + } +}; diff --git a/server/src/internal/platform/honoPlatformRouter.ts b/server/src/internal/platform/honoPlatformRouter.ts index 669a80783..ff8c85497 100644 --- a/server/src/internal/platform/honoPlatformRouter.ts +++ b/server/src/internal/platform/honoPlatformRouter.ts @@ -1,6 +1,5 @@ import { Hono } from "hono"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; -import { listPlatformUsers } from "./handlers/handleListPlatformUsers.js"; /** * Hono router for platform API endpoints @@ -8,4 +7,4 @@ import { listPlatformUsers } from "./handlers/handleListPlatformUsers.js"; export const honoPlatformRouter = new Hono(); // GET /platform/users - List users created by master org -honoPlatformRouter.get("/users", ...listPlatformUsers); +// honoPlatformRouter.get("/users", ...listPlatformUsers); diff --git a/server/src/internal/platform/platformBeta/PLATFORM_API.md b/server/src/internal/platform/platformBeta/PLATFORM_API.md new file mode 100644 index 000000000..69c365487 --- /dev/null +++ b/server/src/internal/platform/platformBeta/PLATFORM_API.md @@ -0,0 +1,224 @@ +# Platform API Reference + +The Platform API allows you to manage organizations and Stripe Connect accounts on behalf of your tenants. All endpoints require platform feature access. + +## Authentication + +All Platform API endpoints require: +- Valid Autumn API key in the `Authorization` header +- Platform feature enabled for your organization + +```bash +Authorization: Bearer am_sk_test_... +``` + +--- + +## Endpoints + +### POST /v1/platform/beta/organization + +Creates a new organization for a platform tenant. Reuses existing users and organizations if they already exist. + +**Request Body:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `user_email` | string | Yes | Email address of the organization owner. User will be created if it doesn't exist. | +| `name` | string | Yes | Display name for the organization. | +| `slug` | string | Yes | Unique slug for the organization (will be prefixed with your org ID). | +| `env` | enum | No | Environment(s) to create API keys for: `"test"`, `"live"`, or `"both"`. Defaults to `"both"`. | + +**Response:** + +| Field | Type | Description | +|-------|------|-------------| +| `test_secret_key` | string? | Autumn test API key for the organization (if `env` is `"test"` or `"both"`). | +| `live_secret_key` | string? | Autumn live API key for the organization (if `env` is `"live"` or `"both"`). | + +**Example:** + +```bash +curl -X POST https://api.useautumn.com/v1/platform/beta/organization \ + -H "Authorization: Bearer am_sk_test_..." \ + -H "Content-Type: application/json" \ + -d '{ + "user_email": "tenant@example.com", + "name": "Tenant Organization", + "slug": "tenant-org", + "env": "both" + }' +``` + +**Response:** +```json +{ + "test_secret_key": "am_sk_test_abc123...", + "live_secret_key": "am_sk_live_xyz789..." +} +``` + +**Notes:** +- If a user with the email already exists, it will be reused +- If an organization with the slug already exists for this user, it will be reused +- The actual organization slug stored will be `{slug}_{your_org_id}` to ensure uniqueness +- Returns Autumn API keys that your tenant can use to interact with Autumn + +--- + +### POST /v1/platform/beta/oauth_url + +Generates a Stripe Connect OAuth URL for a platform organization. Use this to allow your tenants to connect their Stripe accounts. + +**Request Body:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `organization_slug` | string | Yes | The slug of the organization (without the org ID prefix). | +| `env` | enum | Yes | Environment: `"test"` or `"live"`. | +| `redirect_url` | string | Yes | URL to redirect to after OAuth completion. | + +**Response:** + +| Field | Type | Description | +|-------|------|-------------| +| `oauth_url` | string | Stripe Connect OAuth URL to redirect the user to. | + +**Example:** + +```bash +curl -X POST https://api.useautumn.com/v1/platform/beta/oauth_url \ + -H "Authorization: Bearer am_sk_test_..." \ + -H "Content-Type: application/json" \ + -d '{ + "organization_slug": "tenant-org", + "env": "test", + "redirect_url": "https://yourapp.com/stripe/callback" + }' +``` + +**Response:** +```json +{ + "oauth_url": "https://connect.stripe.com/oauth/v2/authorize?response_type=code&client_id=ca_xxx&scope=read_write&state=abc123&redirect_uri=https://express.dev.useautumn.com/stripe/oauth_callback" +} +``` + +**OAuth Flow:** +1. Call this endpoint to get the OAuth URL +2. Redirect your tenant to the `oauth_url` +3. User authorizes their Stripe account +4. Stripe redirects to Autumn's callback URL +5. Autumn processes the authorization and redirects to your `redirect_url` +6. Your `redirect_url` will receive query parameters: + - `success=true` or `success=false` + - `message=...` (if error occurred) + +**Notes:** +- OAuth state is stored in Upstash with 10-minute expiry +- The organization must have been created via the platform API +- After successful OAuth, the Stripe account is automatically linked to the tenant organization + +--- + +### POST /v1/platform/beta/organization/stripe + +Updates a platform organization's Stripe Connect configuration. Associates a Stripe account ID with the organization using your master Stripe credentials. + +**Request Body:** + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `organization_slug` | string | Yes | The slug of the organization (without the org ID prefix). | +| `test_account_id` | string | No* | Stripe account ID for test environment (e.g., `acct_xxx`). | +| `live_account_id` | string | No* | Stripe account ID for live environment (e.g., `acct_xxx`). | + +*At least one of `test_account_id` or `live_account_id` must be provided. + +**Response:** + +| Field | Type | Description | +|-------|------|-------------| +| `message` | string | Success message. | +| `organization.id` | string | Internal organization ID. | +| `organization.slug` | string | Organization slug (without prefix). | + +**Example:** + +```bash +curl -X POST https://api.useautumn.com/v1/platform/beta/organization/stripe \ + -H "Authorization: Bearer am_sk_test_..." \ + -H "Content-Type: application/json" \ + -d '{ + "organization_slug": "tenant-org", + "test_account_id": "acct_1234567890", + "live_account_id": "acct_0987654321" + }' +``` + +**Response:** +```json +{ + "message": "Stripe Connect configuration updated successfully", + "organization": { + "id": "org_abc123", + "slug": "tenant-org" + } +} +``` + +**Validation:** +- Your organization must have the corresponding Stripe secret key connected (test/live) +- The endpoint validates that your master Stripe account can access the provided account ID +- If validation fails, you'll receive a descriptive error message + +**Notes:** +- Use this endpoint when you want to manage Stripe accounts on behalf of your tenants using your own Stripe Connect credentials +- The `master_org_id` is automatically set to your organization ID +- All Stripe operations for the tenant will use your master Stripe credentials with the tenant's account ID +- This is an alternative to the OAuth flow for cases where you have direct access to the tenant's Stripe account ID + +--- + +## Error Responses + +All endpoints return standard error responses: + +```json +{ + "message": "Error description", + "code": "error_code" +} +``` + +### Common Error Codes: + +| Code | Status | Description | +|------|--------|-------------| +| `not_found` | 404 | Organization not found or doesn't exist. | +| `forbidden` | 403 | You don't have permission to manage this organization. | +| `invalid_input` | 400 | Invalid request parameters or missing required fields. | +| `internal_error` | 500 | Internal server error. | +| `not_allowed` | 403 | Platform feature not enabled for your organization. | + +**Example Error Response:** +```json +{ + "message": "Organization with slug 'tenant-org' not found", + "code": "not_found" +} +``` + +--- + +## Rate Limits + +Platform API endpoints share the same rate limits as other Autumn API endpoints. Contact support if you need higher rate limits. + +--- + +## Support + +For questions or issues with the Platform API, contact: +- Email: hey@useautumn.com +- Documentation: https://docs.useautumn.com diff --git a/server/src/internal/platform/platformBeta/handlers/handleCreatePlatformOrg.ts b/server/src/internal/platform/platformBeta/handlers/handleCreatePlatformOrg.ts new file mode 100644 index 000000000..0ef4a5fe8 --- /dev/null +++ b/server/src/internal/platform/platformBeta/handlers/handleCreatePlatformOrg.ts @@ -0,0 +1,159 @@ +import { + AppEnv, + member, + type Organization, + organizations, + user as userTable, +} from "@autumn/shared"; +import { generateId } from "better-auth"; +import { and, eq } from "drizzle-orm"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { UserService } from "@/internal/auth/UserService.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { afterOrgCreated } from "@/utils/authUtils/afterOrgCreated.js"; +import { createKey } from "../../../dev/api-keys/apiKeyUtils.js"; + +const CreateOrganizationSchema = z.object({ + user_email: z.email(), + name: z.string().min(1), + slug: z.string().min(1), + env: z.enum(["test", "live", "both"]).default("both"), +}); + +/** + * Creates an organization for platform users + * - Reuses existing users and organizations + * - Creates test account via Stripe Connect + * - Returns Autumn secret keys + */ +export const handleCreatePlatformOrg = createRoute({ + body: CreateOrganizationSchema, + handler: async (c) => { + const ctx = c.get("ctx"); + const { db, org: masterOrg, logger } = ctx; + + const { user_email, name, slug, env } = c.req.valid("json"); + + // 1. Check if user with this email already exists, otherwise create + let user = await UserService.getByEmail({ + db, + email: user_email, + }); + + if (!user) { + [user] = await db + .insert(userTable) + .values({ + id: generateId(), + name: "", + email: user_email, + emailVerified: true, + createdAt: new Date(), + updatedAt: new Date(), + role: "user", + banned: false, + banReason: null, + banExpires: null, + createdBy: masterOrg.id, + }) + .returning(); + + logger.info(`Created new user: ${user.id} (${user_email})`); + } else { + logger.info( + `[Platform Beta] Found existing user with email: (${user_email})`, + ); + } + + // 2. Check if organization with this slug exists (scoped to master org) + const orgSlug = `${slug}|${masterOrg.id}`; + const existingMembership = await db + .select() + .from(member) + .innerJoin(organizations, eq(member.organizationId, organizations.id)) + .where( + and( + eq(member.userId, user.id), + eq(member.role, "owner"), + eq(organizations.slug, orgSlug), + eq(organizations.created_by, masterOrg.id), + ), + ) + .limit(1); + + const orgExists = OrgService.getBySlug({ + db, + slug: orgSlug, + }); + + let org: Organization; + if (existingMembership.length === 0) { + // Create new organization + const orgId = generateId(); + + console.log(`Creating new organization: ${orgId} (${orgSlug})`); + + [org] = await db + .insert(organizations) + .values({ + id: orgId, + slug: orgSlug, + name, + logo: "", + createdAt: new Date(), + metadata: "", + created_by: masterOrg.id, + }) + .returning(); + + // Create membership + await db.insert(member).values({ + id: generateId(), + organizationId: orgId, + userId: user.id, + role: "owner", + createdAt: new Date(), + }); + + // Initialize org (creates default Stripe test account, svix apps, etc.) + await afterOrgCreated({ org, user }); + + logger.info(`Created new organization: ${org.id} (${orgSlug})`); + } else { + org = existingMembership[0].organizations; + logger.info(`Found existing organization: ${org.id} (${orgSlug})`); + } + + // 3. Generate Autumn secret keys based on env + let test_secret_key: string | undefined; + let live_secret_key: string | undefined; + + if (env === "test" || env === "both") { + test_secret_key = await createKey({ + db, + orgId: org.id, + env: AppEnv.Sandbox, + name: "Platform API Key", + prefix: "am_sk_test", + meta: {}, + }); + } + + if (env === "live" || env === "both") { + live_secret_key = await createKey({ + db, + orgId: org.id, + env: AppEnv.Live, + name: "Platform API Key", + prefix: "am_sk_live", + meta: {}, + }); + } + + return c.json({ + test_secret_key, + live_secret_key, + }); + }, +}); diff --git a/server/src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts b/server/src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts new file mode 100644 index 000000000..2b63c9d48 --- /dev/null +++ b/server/src/internal/platform/platformBeta/handlers/handleGetPlatformOAuth.ts @@ -0,0 +1,74 @@ +import { ErrCode, RecaseError } from "@autumn/shared"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { generateOAuthState } from "../utils/oauthStateUtils.js"; +import { validatePlatformOrg } from "../utils/validatePlatformOrg.js"; + +const GetOAuthUrlSchema = z.object({ + organization_slug: z.string().min(1), + env: z.enum(["test", "live"]), + redirect_url: z.string(), +}); + +/** + * POST /oauth_url + * Generates Stripe OAuth URL for platform organizations + * - Validates organization ownership + * - Generates secure state key stored in Redis + * - Returns OAuth URL with state + */ +export const handleGetPlatformOAuth = createRoute({ + body: GetOAuthUrlSchema, + handler: async (c) => { + const ctx = c.get("ctx"); + const { db, org: masterOrg, logger } = ctx; + + const { organization_slug, env, redirect_url } = c.req.valid("json"); + + // Verify the organization exists and was created by this master org + const org = await validatePlatformOrg({ + db, + organizationSlug: organization_slug, + masterOrg, + }); + + // Generate OAuth state and store in Redis + const stateKey = await generateOAuthState({ + organizationSlug: org.slug, + env, + redirectUri: redirect_url, + masterOrgId: masterOrg.id, + }); + + // Get appropriate Stripe client ID based on environment + const clientId = + env === "live" + ? process.env.STRIPE_LIVE_CLIENT_ID + : process.env.STRIPE_SANDBOX_CLIENT_ID; + + if (!clientId) { + throw new RecaseError({ + message: `Stripe ${env} client ID not configured`, + code: ErrCode.InternalError, + statusCode: 500, + }); + } + + // Build OAuth URL + const oauthUrl = new URL("https://connect.stripe.com/oauth/v2/authorize"); + oauthUrl.searchParams.set("response_type", "code"); + oauthUrl.searchParams.set("client_id", clientId); + oauthUrl.searchParams.set("scope", "read_write"); + oauthUrl.searchParams.set("state", stateKey); + oauthUrl.searchParams.set( + "redirect_uri", + `${process.env.BETTER_AUTH_URL || "https://express.dev.useautumn.com"}/stripe/oauth_callback`, + ); + + logger.info(`Generated OAuth URL for platform org ${org.slug} (${env})`); + + return c.json({ + oauth_url: oauthUrl.toString(), + }); + }, +}); diff --git a/server/src/internal/platform/platformBeta/handlers/handleListPlatformOrgs.ts b/server/src/internal/platform/platformBeta/handlers/handleListPlatformOrgs.ts new file mode 100644 index 000000000..1d49feb11 --- /dev/null +++ b/server/src/internal/platform/platformBeta/handlers/handleListPlatformOrgs.ts @@ -0,0 +1,46 @@ +import { + type ApiPlatformOrg, + type ListPlatformOrgsQuery, + ListPlatformOrgsQuerySchema, + organizations, +} from "@autumn/shared"; +import { eq } from "drizzle-orm"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { toPlatformOrg } from "./platformOrgUtils.js"; + +/** + * Route: GET /platform/orgs - List organizations created by master org + */ +export const handleListPlatformOrgs = createRoute({ + query: ListPlatformOrgsQuerySchema, + handler: async (c) => { + const query = c.req.valid("query") as ListPlatformOrgsQuery; + const ctx = c.get("ctx"); + const { db, org: masterOrg } = ctx; + + const orgs = await db + .select() + .from(organizations) + .where(eq(organizations.created_by, masterOrg.id)) + .limit(query.limit) + .offset(query.offset); + + const orgsList: ApiPlatformOrg[] = orgs.map((org) => + toPlatformOrg({ + org: { + slug: org.slug, + name: org.name, + createdAt: org.createdAt, + }, + masterOrgId: masterOrg.id, + }), + ); + + return c.json({ + list: orgsList, + total: orgs.length, + limit: query.limit, + offset: query.offset, + }); + }, +}); diff --git a/server/src/internal/platform/handlers/handleListPlatformUsers.ts b/server/src/internal/platform/platformBeta/handlers/handleListPlatformUsers.ts similarity index 69% rename from server/src/internal/platform/handlers/handleListPlatformUsers.ts rename to server/src/internal/platform/platformBeta/handlers/handleListPlatformUsers.ts index 8045db43e..e1cc281ef 100644 --- a/server/src/internal/platform/handlers/handleListPlatformUsers.ts +++ b/server/src/internal/platform/platformBeta/handlers/handleListPlatformUsers.ts @@ -9,6 +9,7 @@ import { import { eq } from "drizzle-orm"; import { cte } from "@/db/cteUtils/buildCte.js"; import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { toPlatformOrg } from "./platformOrgUtils.js"; /** * Route: GET /platform/users - List users created by master org @@ -19,7 +20,7 @@ export const listPlatformUsers = createRoute({ const query = c.req.valid("query") as ListPlatformUsersQuery; const ctx = c.get("ctx"); - const { db, org, logger } = ctx; + const { db, org } = ctx; const shouldExpandOrgs = query.expand?.includes("organizations"); @@ -54,11 +55,16 @@ export const listPlatformUsers = createRoute({ created_at: new Date(user.created_at).getTime(), ...(shouldExpandOrgs && user.organizations && { - organizations: user.organizations.map((org: any) => ({ - slug: cleanOrgSlug(org.slug, org.id), - name: org.name, - created_at: new Date(org.createdAt).getTime(), - })), + organizations: user.organizations.map((org: any) => + toPlatformOrg({ + org: { + slug: org.slug, + name: org.name, + createdAt: org.createdAt, + }, + masterOrgId: ctx.org?.id || "", + }), + ), }), })); @@ -70,20 +76,3 @@ export const listPlatformUsers = createRoute({ }); }, }); - -/** - * Remove master org ID prefix from organization slug - */ -function cleanOrgSlug(slug: string, orgId: string): string { - let cleanedSlug = slug; - const prefix = `${orgId}_`; - if (cleanedSlug.startsWith(prefix)) { - cleanedSlug = cleanedSlug.slice(prefix.length); - } - // Handle the case where slug is prepended with "slug_orgId" - const altPrefix = `_${orgId}`; - if (cleanedSlug.endsWith(altPrefix)) { - cleanedSlug = cleanedSlug.slice(0, -altPrefix.length); - } - return cleanedSlug; -} diff --git a/server/src/internal/platform/platformBeta/handlers/handleUpdateOrganizationStripe.ts b/server/src/internal/platform/platformBeta/handlers/handleUpdateOrganizationStripe.ts new file mode 100644 index 000000000..56c1f2e08 --- /dev/null +++ b/server/src/internal/platform/platformBeta/handlers/handleUpdateOrganizationStripe.ts @@ -0,0 +1,125 @@ +import { AppEnv, type Organization, organizations } from "@autumn/shared"; +import { eq } from "drizzle-orm"; +import { z } from "zod/v4"; +import { initPlatformStripe } from "@/external/connect/initStripeCli.js"; +import { registerConnectWebhook } from "@/external/connect/registerConnectWebhook.js"; +import { logger } from "@/external/logtail/logtailUtils.js"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js"; +import { validatePlatformOrg } from "../utils/validatePlatformOrg.js"; + +const UpdateOrganizationStripeSchema = z + .object({ + organization_slug: z.string().min(1), + test_account_id: z.string().optional(), + live_account_id: z.string().optional(), + }) + .refine( + (data) => data.test_account_id || data.live_account_id, + "At least one of test_account_id or live_account_id is required", + ); + +/** + * Validates that master org can access the Stripe account and updates the org's Stripe Connect config + */ +const validateAndUpdateStripeAccount = async ({ + accountId, + env, + masterOrg, + org, +}: { + accountId: string; + env: AppEnv; + masterOrg: Organization; + org: Organization; +}) => { + const stripeCli = initPlatformStripe({ + masterOrg, + env, + accountId, + }); + + const account = await stripeCli.accounts.retrieve(accountId); + logger.info(`Stripe account ${account?.id} retrieved successfully`); + + // Update the organization's Stripe Connect configuration + const currentConnect = + env === AppEnv.Sandbox ? org.test_stripe_connect : org.live_stripe_connect; + + return { + ...currentConnect, + account_id: accountId, + master_org_id: masterOrg.id, + }; +}; + +/** + * POST /organization/stripe + * Updates Stripe Connect account for a platform organization + * - Requires master org to have Stripe secret key connected + * - Validates master org can access the account + * - Stores master_org_id in the tenant org's stripe_connect config + */ +export const handleUpdateOrganizationStripe = createRoute({ + body: UpdateOrganizationStripeSchema, + handler: async (c) => { + const ctx = c.get("ctx"); + const { db, org: masterOrg, logger } = ctx; + + const { organization_slug, test_account_id, live_account_id } = + c.req.valid("json"); + + // Verify the organization exists and was created by this master org + const org = await validatePlatformOrg({ + db, + organizationSlug: organization_slug, + masterOrg, + }); + + // Validate and update Stripe accounts + const updates: { + test_stripe_connect?: any; + live_stripe_connect?: any; + } = {}; + + if (test_account_id) { + updates.test_stripe_connect = await validateAndUpdateStripeAccount({ + accountId: test_account_id, + env: AppEnv.Sandbox, + masterOrg, + org, + }); + } + + if (live_account_id) { + updates.live_stripe_connect = await validateAndUpdateStripeAccount({ + accountId: live_account_id, + env: AppEnv.Live, + masterOrg, + org, + }); + } + + await db + .update(organizations) + .set(updates) + .where(eq(organizations.id, org.id)); + + // Clear organization cache + await clearOrgCache({ db, orgId: org.id }); + + logger.info( + `Updated Stripe Connect for platform org ${org.slug}: test=${test_account_id}, live=${live_account_id}`, + ); + + await registerConnectWebhook({ ctx }); + + return c.json({ + message: "Stripe Connect configuration updated successfully", + organization: { + id: org.id, + slug: organization_slug, + }, + }); + }, +}); diff --git a/server/src/internal/platform/platformBeta/handlers/platformOrgUtils.ts b/server/src/internal/platform/platformBeta/handlers/platformOrgUtils.ts new file mode 100644 index 000000000..0885ff2ca --- /dev/null +++ b/server/src/internal/platform/platformBeta/handlers/platformOrgUtils.ts @@ -0,0 +1,44 @@ +import type { ApiPlatformOrg } from "@autumn/shared"; + +/** + * Remove master org ID prefix from organization slug + */ +function cleanOrgSlug({ + slug, + orgId, +}: { + slug: string; + orgId: string; +}): string { + let cleanedSlug = slug; + const prefix = `${orgId}_`; + if (cleanedSlug.startsWith(prefix)) { + cleanedSlug = cleanedSlug.slice(prefix.length); + } + // Handle the case where slug is prepended with "slug_orgId" + const altPrefix1 = `_${orgId}`; + const altPrefix2 = `|${orgId}`; + if (cleanedSlug.endsWith(altPrefix1)) { + cleanedSlug = cleanedSlug.slice(0, -altPrefix1.length); + } else if (cleanedSlug.endsWith(altPrefix2)) { + cleanedSlug = cleanedSlug = cleanedSlug.slice(0, -altPrefix2.length); + } + return cleanedSlug; +} + +/** + * Convert raw org data to ApiPlatformOrg format + */ +export function toPlatformOrg({ + org, + masterOrgId, +}: { + org: { slug: string; name: string; createdAt: string | Date }; + masterOrgId: string; +}): ApiPlatformOrg { + return { + slug: cleanOrgSlug({ slug: org.slug, orgId: masterOrgId }), + name: org.name, + created_at: new Date(org.createdAt).getTime(), + }; +} diff --git a/server/src/internal/platform/platformBeta/platformBetaRouter.ts b/server/src/internal/platform/platformBeta/platformBetaRouter.ts new file mode 100644 index 000000000..d2cf54de0 --- /dev/null +++ b/server/src/internal/platform/platformBeta/platformBetaRouter.ts @@ -0,0 +1,85 @@ +import { Autumn } from "autumn-js"; +import { Hono } from "hono"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; +import { handleCreatePlatformOrg } from "./handlers/handleCreatePlatformOrg.js"; +import { handleGetPlatformOAuth } from "./handlers/handleGetPlatformOAuth.js"; +import { handleListPlatformOrgs } from "./handlers/handleListPlatformOrgs.js"; +import { listPlatformUsers } from "./handlers/handleListPlatformUsers.js"; +import { handleUpdateOrganizationStripe } from "./handlers/handleUpdateOrganizationStripe.js"; + +const platformBetaRouter = new Hono(); + +/** + * Platform authentication middleware + * Checks if the requesting organization has access to platform API + */ +platformBetaRouter.use("*", async (c, next) => { + const ctx = c.get("ctx"); + const { org, logger } = ctx; + + if (!process.env.AUTUMN_SECRET_KEY) { + return next(); + } + + try { + const autumn = new Autumn(); + const { data, error } = await autumn.check({ + customer_id: org.id, + feature_id: "platform", + }); + + if (error) { + throw error; + } + + if (!data?.allowed) { + return c.json( + { + message: + "You're not allowed to access the platform API. Please contact hey@useautumn.com to request access!", + code: "not_allowed", + }, + 403, + ); + } + + await next(); + } catch (error) { + logger.error(`Failed to check if org is allowed to access platform`, { + error, + }); + return c.json( + { + message: "Failed to check if org is allowed to access platform", + code: "internal_error", + }, + 500, + ); + } +}); + +/** + * POST /organization + * Creates a new organization for platform users + */ +platformBetaRouter.post("/organizations", ...handleCreatePlatformOrg); + +/** + * POST /oauth_url + * Generates Stripe OAuth URL for platform organizations + */ +platformBetaRouter.post("/oauth_url", ...handleGetPlatformOAuth); + +/** + * POST /organization/stripe + * Updates Stripe Connect configuration for platform organization + */ +platformBetaRouter.post( + "/organization/stripe", + ...handleUpdateOrganizationStripe, +); + +platformBetaRouter.get("/users", ...listPlatformUsers); + +platformBetaRouter.get("/organizations", ...handleListPlatformOrgs); +export { platformBetaRouter }; diff --git a/server/src/internal/platform/platformBeta/utils/oauthStateUtils.ts b/server/src/internal/platform/platformBeta/utils/oauthStateUtils.ts new file mode 100644 index 000000000..5e40d127a --- /dev/null +++ b/server/src/internal/platform/platformBeta/utils/oauthStateUtils.ts @@ -0,0 +1,104 @@ +import { randomBytes } from "node:crypto"; +import { InternalError } from "@autumn/shared"; +import { initUpstash } from "@/internal/customers/cusCache/upstashUtils.js"; + +const STATE_KEY_PREFIX = "oauth_state:"; +const STATE_EXPIRY_SECONDS = 10 * 60; // 10 minutes + +export type OAuthState = { + organization_slug: string; + env: "test" | "live"; + redirect_uri: string; + master_org_id: string | null; // null for standard flow, string for platform flow +}; + +/** + * Generates a unique OAuth state key and stores it in Upstash + * Retries up to 3 times if key already exists (race condition prevention) + */ +export const generateOAuthState = async ({ + organizationSlug, + env, + redirectUri, + masterOrgId, +}: { + organizationSlug: string; + env: "test" | "live"; + redirectUri: string; + masterOrgId: string | null; +}): Promise => { + const upstash = await initUpstash(); + if (!upstash) { + throw new InternalError({ + message: "Upstash not configured", + code: "upstash_not_configured", + }); + } + + const maxAttempts = 3; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + // Generate random state key + const stateKey = randomBytes(32).toString("hex"); + const redisKey = `${STATE_KEY_PREFIX}${stateKey}`; + + // Try to set the key + const stateData: OAuthState = { + organization_slug: organizationSlug, + env, + redirect_uri: redirectUri, + master_org_id: masterOrgId, + }; + + // Check if key exists first + const existing = await upstash.get(redisKey); + if (!existing) { + // Key doesn't exist, set it with expiry + await upstash.set(redisKey, stateData, { ex: STATE_EXPIRY_SECONDS }); + return stateKey; + } + + // Key already exists, retry + if (attempt < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, 50)); // Wait 50ms before retry + } + } + + throw new InternalError({ + message: + "Failed to generate unique OAuth state after 3 attempts. Please try again.", + code: "oauth_state_generation_failed", + }); +}; + +/** + * Retrieves and deletes OAuth state from Upstash + * Returns null if state doesn't exist or has expired + */ +export const consumeOAuthState = async ({ + stateKey, +}: { + stateKey: string; +}): Promise => { + const upstash = await initUpstash(); + if (!upstash) { + throw new InternalError({ + message: "Upstash not configured", + code: "upstash_not_configured", + }); + } + + const redisKey = `${STATE_KEY_PREFIX}${stateKey}`; + + // Get the data + const stateData = await upstash.get(redisKey); + + if (!stateData) { + return null; + } + + // Delete the key + await upstash.del(redisKey); + + return stateData; +}; diff --git a/server/src/internal/platform/platformBeta/utils/platformUtils.ts b/server/src/internal/platform/platformBeta/utils/platformUtils.ts new file mode 100644 index 000000000..8ce089fe7 --- /dev/null +++ b/server/src/internal/platform/platformBeta/utils/platformUtils.ts @@ -0,0 +1,9 @@ +export const getConnectedOrgSlug = ({ + orgSlug, + masterOrgId, +}: { + orgSlug: string; + masterOrgId: string; +}) => { + return `${orgSlug}|${masterOrgId}`; +}; diff --git a/server/src/internal/platform/platformBeta/utils/validatePlatformOrg.ts b/server/src/internal/platform/platformBeta/utils/validatePlatformOrg.ts new file mode 100644 index 000000000..86864d3e9 --- /dev/null +++ b/server/src/internal/platform/platformBeta/utils/validatePlatformOrg.ts @@ -0,0 +1,49 @@ +import { type Organization, organizations, RecaseError } from "@autumn/shared"; +import { and, eq } from "drizzle-orm"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { getConnectedOrgSlug } from "./platformUtils.js"; + +/** + * Validates that a platform organization exists and is owned by the master org + * @returns The organization if valid + * @throws RecaseError if org not found or not owned by master + */ +export const validatePlatformOrg = async ({ + db, + organizationSlug, + masterOrg, +}: { + db: DrizzleCli; + organizationSlug: string; + masterOrg: Organization; +}): Promise => { + const orgSlug = getConnectedOrgSlug({ + orgSlug: organizationSlug, + masterOrgId: masterOrg.id, + }); + + const [org] = await db + .select() + .from(organizations) + .where( + and( + eq(organizations.slug, orgSlug), + eq(organizations.created_by, masterOrg.id), + ), + ) + .limit(1); + + if (!org) { + throw new RecaseError({ + message: `Organization with slug '${organizationSlug}' not found`, + }); + } + + if (org.created_by !== masterOrg.id) { + throw new RecaseError({ + message: "You do not have permission to manage this organization", + }); + } + + return org; +}; diff --git a/server/src/internal/platform/platformRouter.ts b/server/src/internal/platform/platformLegacy/platformRouter.ts similarity index 94% rename from server/src/internal/platform/platformRouter.ts rename to server/src/internal/platform/platformLegacy/platformRouter.ts index d7a1cc634..c583555a5 100644 --- a/server/src/internal/platform/platformRouter.ts +++ b/server/src/internal/platform/platformLegacy/platformRouter.ts @@ -11,13 +11,12 @@ import { generateId } from "better-auth"; import { and, eq } from "drizzle-orm"; import { type NextFunction, Router } from "express"; import { z } from "zod"; +import { createKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; +import { connectStripe } from "@/internal/orgs/handlers/handleConnectStripe_old.js"; +import { shouldReconnectStripe } from "@/internal/orgs/orgUtils.js"; import { afterOrgCreated } from "@/utils/authUtils/afterOrgCreated.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; import { routeHandler } from "@/utils/routerUtils.js"; -import { createKey } from "../dev/api-keys/apiKeyUtils.js"; -import { connectStripe } from "../orgs/handlers/handleConnectStripe.js"; - -import { shouldReconnectStripe } from "../orgs/orgUtils.js"; const platformRouter = Router(); @@ -190,7 +189,7 @@ platformRouter.post("/exchange", (req: any, res: any) => createdAt: new Date(), }); - await afterOrgCreated({ org }); + await afterOrgCreated({ org, user, createStripeAccount: false }); } else { // org = (await db.query.organizations.findFirst({ // where: eq(organizations.id, membership.organizationId), @@ -198,9 +197,10 @@ platformRouter.post("/exchange", (req: any, res: any) => org = membership.organizations as Organization; } - let sandboxKey, prodKey; + let sandboxKey: string | undefined; + let prodKey: string | undefined; - let finalStripeConfig: any = {}; + let finalStripeConfig: StripeConfig = {}; let defaultCurrency = org.default_currency || "usd"; // Connect stripe if not exists... @@ -209,7 +209,7 @@ platformRouter.post("/exchange", (req: any, res: any) => org, env: AppEnv.Sandbox, stripeKey: stripe_test_key, - logger: req.logtail, + logger: req.logger, }); if (reconnectStripe) { diff --git a/server/src/internal/products/handlers/handleCopyProduct.ts b/server/src/internal/products/handlers/handleCopyProduct.ts index 0ee1f0d01..98b65ca59 100644 --- a/server/src/internal/products/handlers/handleCopyProduct.ts +++ b/server/src/internal/products/handlers/handleCopyProduct.ts @@ -16,7 +16,7 @@ export const handleCopyProduct = async (req: any, res: any) => res, action: "Copy Product", handler: async (req, res) => { - const { db, logtail: logger } = req; + const { db, logger } = req; const { productId: fromProductId } = req.params; const orgId = req.orgId; diff --git a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts index 35b541ac2..d6c5b051a 100644 --- a/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts +++ b/server/src/internal/products/handlers/handleUpdateProduct/handleUpdateProduct.ts @@ -172,6 +172,7 @@ export const handleUpdateProductV2 = createRoute({ } // New full product + await initProductInStripe({ db, product: newFullProduct, diff --git a/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts b/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts index f9beea3be..dd6be3462 100644 --- a/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts +++ b/server/src/internal/products/handlers/handleUpdateProduct/updateProductDetails.ts @@ -12,7 +12,7 @@ import { type UpdateProduct, } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; import { isStripeConnected } from "@/internal/orgs/orgUtils.js"; import { notNullish } from "@/utils/genUtils.js"; diff --git a/server/src/internal/products/internalHandlers/handleGetProductCount.ts b/server/src/internal/products/internalHandlers/handleGetProductCount.ts new file mode 100644 index 000000000..0e2d0102d --- /dev/null +++ b/server/src/internal/products/internalHandlers/handleGetProductCount.ts @@ -0,0 +1,42 @@ +import { ProductNotFoundError } from "@autumn/shared"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { CusProdReadService } from "@/internal/customers/cusProducts/CusProdReadService.js"; +import { ProductService } from "@/internal/products/ProductService.js"; + +const GetProductCountQuerySchema = z.object({ + version: z.coerce.number().optional(), +}); + +/** + * GET /products/:productId/count + * Get customer counts for a specific product version + */ +export const handleGetProductCount = createRoute({ + query: GetProductCountQuerySchema, + handler: async (c) => { + const ctx = c.get("ctx"); + const { productId } = c.req.param(); + const { version } = c.req.valid("query"); + + const product = await ProductService.get({ + db: ctx.db, + id: productId, + orgId: ctx.org.id, + env: ctx.env, + version: version, + }); + + if (!product) { + throw new ProductNotFoundError({ productId, version }); + } + + // Get counts from postgres + const counts = await CusProdReadService.getCounts({ + db: ctx.db, + internalProductId: product.internal_id, + }); + + return c.json(counts); + }, +}); diff --git a/server/src/internal/products/internalHandlers/handleGetProductInternal.ts b/server/src/internal/products/internalHandlers/handleGetProductInternal.ts new file mode 100644 index 000000000..5fbd29d65 --- /dev/null +++ b/server/src/internal/products/internalHandlers/handleGetProductInternal.ts @@ -0,0 +1,40 @@ +import { mapToProductV2, queryInteger } from "@autumn/shared"; +import { z } from "zod/v4"; +import { createRoute } from "@/honoMiddlewares/routeHandler.js"; +import { ProductService } from "../ProductService.js"; + +const GetProductInternalQuerySchema = z.object({ + version: queryInteger().optional(), +}); + +export const handleGetProductInternal = createRoute({ + query: GetProductInternalQuerySchema, + handler: async (c) => { + const { productId } = c.req.param(); + const { version } = c.req.valid("query"); + const { db, org, env, features } = c.get("ctx"); + + const [product, latestProduct] = await Promise.all([ + ProductService.getFull({ + db, + idOrInternalId: productId, + orgId: org.id, + env, + version: version, + }), + ProductService.getFull({ + db, + idOrInternalId: productId, + orgId: org.id, + env, + }), + ]); + + const productV2 = mapToProductV2({ + product: product, + features: features, + }); + + return c.json({ product: productV2, numVersions: latestProduct.version }); + }, +}); diff --git a/server/src/internal/products/internalProductRouter.ts b/server/src/internal/products/internalProductRouter.ts index 4746509ac..d2a57d735 100644 --- a/server/src/internal/products/internalProductRouter.ts +++ b/server/src/internal/products/internalProductRouter.ts @@ -1,8 +1,4 @@ -import { - type FeatureOptions, - ProductNotFoundError, - UsageModel, -} from "@autumn/shared"; +import { type FeatureOptions, UsageModel } from "@autumn/shared"; import { Router } from "express"; import { handleFrontendReqError } from "@/utils/errorUtils.js"; import { CusProdReadService } from "../customers/cusProducts/CusProdReadService.js"; @@ -24,10 +20,10 @@ import { } from "./productUtils.js"; import { mapToProductV2 } from "./productV2Utils.js"; -export const productRouter: Router = Router({ mergeParams: true }); +export const expressProductRouter: Router = Router({ mergeParams: true }); // Get list of products -productRouter.get("/products", async (req: any, res) => { +expressProductRouter.get("/products", async (req: any, res) => { try { const { db } = req; const products = await ProductService.listFull({ @@ -55,7 +51,7 @@ productRouter.get("/products", async (req: any, res) => { }); // Get counts for all products -productRouter.get("/product_counts", async (req: any, res) => { +expressProductRouter.get("/product_counts", async (req: any, res) => { try { const { db } = req; const products = await ProductService.listFull({ @@ -90,7 +86,7 @@ productRouter.get("/product_counts", async (req: any, res) => { }); // Get list of features -productRouter.get("/features", async (req: any, res) => { +expressProductRouter.get("/features", async (req: any, res) => { try { res.status(200).json({ features: req.features }); } catch (error) { @@ -100,7 +96,7 @@ productRouter.get("/features", async (req: any, res) => { }); // Get list of rewards -productRouter.get("/rewards", async (req: any, res) => { +expressProductRouter.get("/rewards", async (req: any, res) => { try { const { db, orgId, env } = req; const rewards = await RewardService.list({ db, orgId, env }); @@ -120,110 +116,81 @@ productRouter.get("/rewards", async (req: any, res) => { } }); -// Get single product data -productRouter.get("/:productId/data2", async (req: any, res) => { - try { - const { productId } = req.params; - const { version } = req.query; - const { db, orgId, env } = req; +// // Get single product data +// expressProductRouter.get("/:productId/data2", async (req: any, res) => { +// try { +// const { productId } = req.params; +// const { version } = req.query; +// const { db, orgId, env } = req; - console.log("[/data2] Request params:", { - productId, - version, - orgId, - env, - featuresLength: req.features?.length, - }); +// const [product, latestProduct] = await Promise.all([ +// ProductService.getFull({ +// db, +// idOrInternalId: productId, +// orgId, +// env, +// version: version ? parseInt(version) : undefined, +// }), +// ProductService.getFull({ +// db, +// idOrInternalId: productId, +// orgId, +// env, +// }), +// ]); - const [product, latestProduct] = await Promise.all([ - ProductService.getFull({ - db, - idOrInternalId: productId, - orgId, - env, - version: version ? parseInt(version, 10) : undefined, - }), - ProductService.getFull({ - db, - idOrInternalId: productId, - orgId, - env, - }), - ]); +// const productV2 = mapToProductV2({ +// product: product, +// features: req.features, +// }); - if (!product) { - throw new ProductNotFoundError({ productId, version }); - } +// res +// .status(200) +// .json({ product: productV2, numVersions: latestProduct.version }); +// } catch (error) { +// console.error("Failed to get product", error); +// res.status(500).send(error); +// } +// }); - console.log( - "[/data2] Product found:", - product.id, - "Features available:", - req.features?.length || 0, - ); +// // Get counts for a single product +// expressProductRouter.get("/:productId/count", async (req: any, res) => { +// try { +// const { db, orgId, env } = req; +// const { productId } = req.params; +// const { version } = req.query; - const productV2 = mapToProductV2({ - product: product, - features: req.features || [], - }); +// const product = await ProductService.get({ +// db, +// id: productId, +// orgId, +// env, +// version: version ? parseInt(version) : undefined, +// }); - res.status(200).json({ - product: { - ...productV2, - archived: latestProduct.archived, - }, - numVersions: latestProduct.version, - }); - } catch (error: any) { - console.error("Failed to get product", error); - console.error("Error details:", { - message: error?.message, - stack: error?.stack, - name: error?.name, - }); - res.status(500).send(error); - } -}); +// if (!product) { +// throw new ProductNotFoundError({ productId, version }); +// } -// Get counts for a single product -productRouter.get("/:productId/count", async (req: any, res) => { - try { - const { db, orgId, env } = req; - const { productId } = req.params; - const { version } = req.query; +// // Get counts from postgres +// const counts = await CusProdReadService.getCounts({ +// db, +// internalProductId: product.internal_id, +// }); - const product = await ProductService.get({ - db, - id: productId, - orgId, - env, - version: version ? parseInt(version) : undefined, - }); - - if (!product) { - throw new ProductNotFoundError({ productId, version }); - } - - // Get counts from postgres - - const counts = await CusProdReadService.getCounts({ - db, - internalProductId: product.internal_id, - }); - - res.status(200).send(counts); - } catch (error) { - handleFrontendReqError({ - error, - req, - res, - action: "Get product counts (internal)", - }); - } -}); +// res.status(200).send(counts); +// } catch (error) { +// handleFrontendReqError({ +// error, +// req, +// res, +// action: "Get product counts (internal)", +// }); +// } +// }); // Get list of migrations -productRouter.get("/migrations", async (req: any, res) => { +expressProductRouter.get("/migrations", async (req: any, res) => { try { const { db, orgId, env } = req; const migrations = await MigrationService.getExistingJobs({ @@ -242,7 +209,7 @@ productRouter.get("/migrations", async (req: any, res) => { } }); -productRouter.get("/data", async (req: any, res) => { +expressProductRouter.get("/data", async (req: any, res) => { try { const { db } = req; @@ -297,7 +264,7 @@ productRouter.get("/data", async (req: any, res) => { } }); -productRouter.post("/data", async (req: any, res) => { +expressProductRouter.post("/data", async (req: any, res) => { try { const { db } = req; const { showArchived } = req.body; @@ -348,7 +315,7 @@ productRouter.post("/data", async (req: any, res) => { } }); -productRouter.get("/counts", async (req: any, res) => { +expressProductRouter.get("/counts", async (req: any, res) => { try { const { db } = req; const products = await ProductService.listFull({ @@ -392,91 +359,91 @@ productRouter.get("/counts", async (req: any, res) => { } }); -productRouter.get("/:productId/data", async (req: any, res) => { - try { - const { productId } = req.params; - const { version } = req.query; - const { db, orgId, env } = req; +// expressProductRouter.get("/:productId/data", async (req: any, res) => { +// try { +// const { productId } = req.params; +// const { version } = req.query; +// const { db, orgId, env } = req; - const [product, features, org, numVersions, existingMigrations] = - await Promise.all([ - ProductService.getFull({ - db, - idOrInternalId: productId, - orgId, - env, - version: version ? parseInt(version) : undefined, - }), - FeatureService.getFromReq(req), - OrgService.getFromReq(req), - ProductService.getProductVersionCount({ - db, - productId, - orgId, - env, - }), - MigrationService.getExistingJobs({ - db, - orgId, - env, - }), - ]); +// const [product, features, org, numVersions, existingMigrations] = +// await Promise.all([ +// ProductService.getFull({ +// db, +// idOrInternalId: productId, +// orgId, +// env, +// version: version ? parseInt(version) : undefined, +// }), +// FeatureService.getFromReq(req), +// OrgService.getFromReq(req), +// ProductService.getProductVersionCount({ +// db, +// productId, +// orgId, +// env, +// }), +// MigrationService.getExistingJobs({ +// db, +// orgId, +// env, +// }), +// ]); - if (!product) { - throw new ProductNotFoundError({ productId, version }); - } +// if (!product) { +// throw new ProductNotFoundError({ productId, version }); +// } - const defaultProds = await ProductService.listDefault({ - db, - orgId: req.orgId, - env: req.env, - group: product.group, - }); +// const defaultProds = await ProductService.listDefault({ +// db, +// orgId: req.orgId, +// env: req.env, +// group: product.group, +// }); - const groupDefaults = getGroupToDefaults({ - defaultProds, - })?.[product.group]; +// const groupDefaults = getGroupToDefaults({ +// defaultProds, +// })?.[product.group]; - let entitlements = product.entitlements; - let prices = product.prices; +// let entitlements = product.entitlements; +// let prices = product.prices; - entitlements = entitlements.sort((a: any, b: any) => { - return b.feature.id.localeCompare(a.feature.id); - }); +// entitlements = entitlements.sort((a: any, b: any) => { +// return b.feature.id.localeCompare(a.feature.id); +// }); - prices = prices.sort((a: any, b: any) => { - return b.id.localeCompare(a.id); - }); +// prices = prices.sort((a: any, b: any) => { +// return b.id.localeCompare(a.id); +// }); - const productV2 = mapToProductV2({ product, features }); +// const productV2 = mapToProductV2({ product, features }); - res.status(200).send({ - product: productV2, - entitlements, - prices, - features, - org: { - id: org.id, - name: org.name, - test_pkey: org.test_pkey, - live_pkey: org.live_pkey, - default_currency: org.default_currency, - }, - numVersions, - existingMigrations, - groupDefaults: groupDefaults, - }); - } catch (error) { - handleFrontendReqError({ - error, - req, - res, - action: "Get product data (internal)", - }); - } -}); +// res.status(200).send({ +// product: productV2, +// entitlements, +// prices, +// features, +// org: { +// id: org.id, +// name: org.name, +// test_pkey: org.test_pkey, +// live_pkey: org.live_pkey, +// default_currency: org.default_currency, +// }, +// numVersions, +// existingMigrations, +// groupDefaults: groupDefaults, +// }); +// } catch (error) { +// handleFrontendReqError({ +// error, +// req, +// res, +// action: "Get product data (internal)", +// }); +// } +// }); -productRouter.post("/product_options", async (req: any, res: any) => { +expressProductRouter.post("/product_options", async (req: any, res: any) => { try { const { items } = req.body; @@ -502,9 +469,9 @@ productRouter.post("/product_options", async (req: any, res: any) => { } }); -productRouter.get("/:productId/info", handleGetProductDeleteInfo); +expressProductRouter.get("/:productId/info", handleGetProductDeleteInfo); -productRouter.get("/rewards", async (req: any, res: any) => { +expressProductRouter.get("/rewards", async (req: any, res: any) => { try { const { db, orgId, env } = req; @@ -525,23 +492,37 @@ productRouter.get("/rewards", async (req: any, res: any) => { } }); -productRouter.get("/has_entity_feature_id", async (req: any, res: any) => { - try { - const { db, orgId, env } = req; +expressProductRouter.get( + "/has_entity_feature_id", + async (req: any, res: any) => { + try { + const { db, orgId, env } = req; - const hasEntityFeatureId = await EntitlementService.hasEntityFeatureId({ - db, - orgId, - env, - }); + const hasEntityFeatureId = await EntitlementService.hasEntityFeatureId({ + db, + orgId, + env, + }); - res.status(200).send({ hasEntityFeatureId }); - } catch (error) { - handleFrontendReqError({ - error, - req, - res, - action: "Check has entity feature id", - }); - } -}); + res.status(200).send({ hasEntityFeatureId }); + } catch (error) { + handleFrontendReqError({ + error, + req, + res, + action: "Check has entity feature id", + }); + } + }, +); + +import { Hono } from "hono"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; +import { handleGetProductCount } from "./internalHandlers/handleGetProductCount.js"; +import { handleGetProductInternal } from "./internalHandlers/handleGetProductInternal.js"; + +// Hono router for internal/dashboard product routes +export const internalProductRouter = new Hono(); + +internalProductRouter.get("/:productId/count", ...handleGetProductCount); +internalProductRouter.get("/:productId/data", ...handleGetProductInternal); diff --git a/server/src/internal/products/productRouter.ts b/server/src/internal/products/productRouter.ts index 8cef7819d..ccbb18e84 100644 --- a/server/src/internal/products/productRouter.ts +++ b/server/src/internal/products/productRouter.ts @@ -1,8 +1,8 @@ import { ProductNotFoundError } from "@autumn/shared"; import { Router } from "express"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { checkStripeProductExists } from "@/internal/products/productUtils.js"; @@ -33,7 +33,7 @@ productRouter.post("/:productId/copy", handleCopyProduct); productRouter.post("/all/init_stripe", async (req: any, res) => { try { - const { orgId, env, logtail: logger, db } = req; + const { orgId, env, logger, db } = req; const [fullProducts, org] = await Promise.all([ ProductService.listFull({ diff --git a/server/src/internal/products/productUtils.ts b/server/src/internal/products/productUtils.ts index 83144d8b0..ddadd9050 100644 --- a/server/src/internal/products/productUtils.ts +++ b/server/src/internal/products/productUtils.ts @@ -22,8 +22,8 @@ import { } from "@autumn/shared"; import { Decimal } from "decimal.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createStripePriceIFNotExist } from "@/external/stripe/createStripePrice/createStripePrice.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { getBillingInterval, getBillingType, diff --git a/server/src/internal/rewards/referralUtils.ts b/server/src/internal/rewards/referralUtils.ts index 88ca95884..08a663895 100644 --- a/server/src/internal/rewards/referralUtils.ts +++ b/server/src/internal/rewards/referralUtils.ts @@ -1,40 +1,22 @@ import { type AppEnv, - AttachBranch, - type Customer, ErrCode, - type FullRewardProgram, type ReferralCode, type Reward, - RewardProgram, - RewardReceivedBy, + type RewardProgram, type RewardRedemption, } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; 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 { 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 { RewardProgramService } from "./RewardProgramService.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"; import { receivedByRedeemer, receivedByReferrer, - triggerFreePaidProduct, } from "./referralUtils/triggerFreePaidProduct.js"; export const ReferralResponseCodes = { @@ -103,7 +85,7 @@ export const triggerRedemption = async ({ let applied = false; let redeemerApplied = false; for (let i = 0; i < 2; i++) { - let customer = i === 0 ? referrer : redeemer; + const customer = i === 0 ? referrer : redeemer; if (i === 0 && !receivedByReferrer(rewardProgram.received_by)) { continue; @@ -119,7 +101,7 @@ export const triggerRedemption = async ({ }); } - let stripeCli = createStripeCli({ + const stripeCli = createStripeCli({ org, env, legacyVersion: true, @@ -133,14 +115,14 @@ export const triggerRedemption = async ({ logger, }); - let stripeCusId = customer.processor.id; - let stripeCus = (await stripeCli.customers.retrieve( + const stripeCusId = customer.processor.id; + const stripeCus = (await stripeCli.customers.retrieve( stripeCusId, )) as Stripe.Customer; if (!stripeCus.discount) { await stripeCli.customers.update(stripeCusId, { - // @ts-ignore + // @ts-expect-error coupon: reward.id, }); @@ -154,7 +136,7 @@ export const triggerRedemption = async ({ } } - let updatedRedemption = await RewardRedemptionService.update({ + const updatedRedemption = await RewardRedemptionService.update({ db, id: redemption.id, updates: { diff --git a/server/src/internal/rewards/referralUtils/triggerFreePaidProduct.ts b/server/src/internal/rewards/referralUtils/triggerFreePaidProduct.ts index 00b2c3f17..b77402d6b 100644 --- a/server/src/internal/rewards/referralUtils/triggerFreePaidProduct.ts +++ b/server/src/internal/rewards/referralUtils/triggerFreePaidProduct.ts @@ -11,8 +11,8 @@ import { } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; import type Stripe from "stripe"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; 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"; diff --git a/server/src/internal/rewards/referralUtils/triggerFreeProduct.ts b/server/src/internal/rewards/referralUtils/triggerFreeProduct.ts index aa15cc92c..0c0edea09 100644 --- a/server/src/internal/rewards/referralUtils/triggerFreeProduct.ts +++ b/server/src/internal/rewards/referralUtils/triggerFreeProduct.ts @@ -69,7 +69,6 @@ export const triggerFreeProduct = async ({ 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; } diff --git a/server/src/internal/rewards/triggerCheckoutReward.ts b/server/src/internal/rewards/triggerCheckoutReward.ts index 6e0580942..8be96a545 100644 --- a/server/src/internal/rewards/triggerCheckoutReward.ts +++ b/server/src/internal/rewards/triggerCheckoutReward.ts @@ -7,7 +7,7 @@ import { RewardTriggerEvent, } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { RewardProgramService } from "../rewards/RewardProgramService.js"; import { RewardRedemptionService } from "./RewardRedemptionService.js"; import { triggerFreeProduct } from "./referralUtils/triggerFreeProduct.js"; diff --git a/server/src/middleware/analyticsMiddleware.ts b/server/src/middleware/analyticsMiddleware.ts index 5d7abc8db..fe222855c 100644 --- a/server/src/middleware/analyticsMiddleware.ts +++ b/server/src/middleware/analyticsMiddleware.ts @@ -7,7 +7,7 @@ const handleResFinish = (req: any, res: any) => { } if (process.env.NODE_ENV !== "development") { - req.logtail.info( + req.logger.info( `[${res.statusCode}] ${req.method} ${req.originalUrl} (${req.org?.slug})`, { statusCode: res.statusCode, @@ -16,7 +16,7 @@ const handleResFinish = (req: any, res: any) => { ); } } catch (error) { - console.error("Failed to log response to logtailAll"); + console.error("Failed to log response"); console.error(error); } }; @@ -60,7 +60,7 @@ export const analyticsMiddleware = async (req: any, res: any, next: any) => { }); } - req.logtail = req.logtail.child({ + req.logger = req.logger.child({ context: { context: reqContext, }, diff --git a/server/src/middleware/apiAuthMiddleware.ts b/server/src/middleware/apiAuthMiddleware.ts index 1774da94c..1bf498e45 100644 --- a/server/src/middleware/apiAuthMiddleware.ts +++ b/server/src/middleware/apiAuthMiddleware.ts @@ -84,7 +84,7 @@ export const verifySecretKey = async (req: any, res: any, next: any) => { }; export const apiAuthMiddleware = async (req: any, res: any, next: any) => { - const logger = req.logtail; + const logger = req.logger; if (trmnlExclusions.includes(req.path)) { logger.info( diff --git a/server/src/middleware/authMiddleware.ts b/server/src/middleware/authMiddleware.ts index c9c0704c5..e85eec3b0 100644 --- a/server/src/middleware/authMiddleware.ts +++ b/server/src/middleware/authMiddleware.ts @@ -1,11 +1,9 @@ -import { OrgService } from "@/internal/orgs/OrgService.js"; -import { auth } from "@/utils/auth.js"; import { AuthType, ErrCode } from "@autumn/shared"; import { verifyToken } from "@clerk/express"; import { fromNodeHeaders } from "better-auth/node"; -import { NextFunction } from "express"; -import { eq, and } from "drizzle-orm"; -import { member } from "@autumn/shared"; +import type { NextFunction } from "express"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { auth } from "@/utils/auth.js"; const getTokenData = async (req: any, res: any) => { let token; @@ -19,10 +17,10 @@ const getTokenData = async (req: any, res: any) => { throw new Error("clerk token not found in request headers / invalid"); } - let secretKey = process.env.CLERK_SECRET_KEY; + const secretKey = process.env.CLERK_SECRET_KEY; try { - let verified = await verifyToken(token, { + const verified = await verifyToken(token, { secretKey: secretKey, }); @@ -37,7 +35,7 @@ const getTokenData = async (req: any, res: any) => { }; export const withOrgAuth = async (req: any, res: any, next: NextFunction) => { - const { logtail: logger } = req; + const { logger } = req; try { // let tokenData = await getTokenData(req, res); @@ -69,7 +67,7 @@ export const withOrgAuth = async (req: any, res: any, next: NextFunction) => { .json({ message: "Unauthorized - no user id found" }); } - let data = await OrgService.getWithFeatures({ + const data = await OrgService.getWithFeatures({ db: req.db, orgId: orgId, env: req.env, diff --git a/server/src/middleware/trmnlAuthMiddleware.ts b/server/src/middleware/trmnlAuthMiddleware.ts index 4e348b96b..3e9b7b721 100644 --- a/server/src/middleware/trmnlAuthMiddleware.ts +++ b/server/src/middleware/trmnlAuthMiddleware.ts @@ -1,8 +1,7 @@ -import { ExtendedResponse } from "@/utils/models/Request.js"; import { AppEnv, ErrCode } from "@autumn/shared"; -import { readFile } from "@/external/supabase/storageUtils.js"; -import { FeatureService } from "@/internal/features/FeatureService.js"; import { initUpstash } from "@/internal/customers/cusCache/upstashUtils.js"; +import { FeatureService } from "@/internal/features/FeatureService.js"; +import type { ExtendedResponse } from "@/utils/models/Request.js"; export const trmnlExclusions = ["/trmnl/screen"]; @@ -66,8 +65,6 @@ export const trmnlAuthMiddleware = async ( }; req.features = features; - // const logger = req.logtail; - // const file = await readFile({ bucket: "private", path: "trmnl.json" }); // const fileString = await file.text(); // const fileJson = JSON.parse(fileString); diff --git a/server/src/queue/workersInit.ts b/server/src/queue/workersInit.ts index 13c7ac6a8..ac33276f2 100644 --- a/server/src/queue/workersInit.ts +++ b/server/src/queue/workersInit.ts @@ -39,7 +39,7 @@ const initWorker = ({ const worker = new Worker( "autumn", async (job: Job) => { - const logtail = logger.child({ + const workerLogger = logger.child({ context: { worker: { task: job.name, @@ -55,7 +55,7 @@ const initWorker = ({ await detectBaseVariant({ db, curProduct: job.data.curProduct, - logger: logtail as Logger, + logger: workerLogger as Logger, }); return; } @@ -64,7 +64,7 @@ const initWorker = ({ await runSaveFeatureDisplayTask({ db, feature: job.data.feature, - logger: logtail, + logger: workerLogger, }); return; } @@ -73,7 +73,7 @@ const initWorker = ({ await runMigrationTask({ db, payload: job.data, - logger: logtail, + logger: workerLogger, }); return; } @@ -82,7 +82,7 @@ const initWorker = ({ await runActionHandlerTask({ queue, job, - logger: logtail, + logger: workerLogger, db, useBackup, }); @@ -93,11 +93,11 @@ const initWorker = ({ await runRewardMigrationTask({ db, payload: job.data, - logger: logtail, + logger: workerLogger, }); } } catch (error: any) { - logtail.error(`Failed to process bullmq job: ${job.name}`, { + workerLogger.error(`Failed to process bullmq job: ${job.name}`, { jobName: job.name, error: { message: error.message, @@ -126,7 +126,7 @@ const initWorker = ({ await runTriggerCheckoutReward({ db, payload: job.data, - logger: logtail, + logger: workerLogger, }); } catch (error) { console.error("Error processing job:", error); @@ -157,13 +157,13 @@ const initWorker = ({ if (job.name === JobName.UpdateBalance) { await runUpdateBalanceTask({ payload: job.data, - logger: logtail, + logger: workerLogger, db, }); } else if (job.name === JobName.UpdateUsage) { await runUpdateUsageTask({ payload: job.data, - logger: logtail, + logger: workerLogger, db, }); } diff --git a/server/src/trigger/adjustAllowance.ts b/server/src/trigger/adjustAllowance.ts index 66e1277c9..1ed281370 100644 --- a/server/src/trigger/adjustAllowance.ts +++ b/server/src/trigger/adjustAllowance.ts @@ -1,32 +1,28 @@ import { - Entitlement, - ErrCode, - FullCusEntWithFullCusProduct, - FullCusEntWithProduct, - Price, -} from "@autumn/shared"; -import { - AppEnv, + type AppEnv, BillingType, - Customer, - Feature, - FullCustomerPrice, - Organization, - UsagePriceConfig, + type Customer, + type Entitlement, + ErrCode, + type Feature, + type FullCusEntWithFullCusProduct, + type FullCustomerPrice, + type Organization, + type Price, + type UsagePriceConfig, } from "@autumn/shared"; - +import { Decimal } from "decimal.js"; +import { StatusCodes } from "http-status-codes"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; +import { getUsageBasedSub } from "@/external/stripe/stripeSubUtils.js"; import { getRelatedCusPrice } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; import { getBillingType } from "@/internal/products/prices/priceUtils.js"; -import { getUsageBasedSub } from "@/external/stripe/stripeSubUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { Decimal } from "decimal.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; -import { handleProratedUpgrade } from "./arrearProratedUsage/handleProratedUpgrade.js"; -import Stripe from "stripe"; -import { handleProratedDowngrade } from "./arrearProratedUsage/handleProratedDowngrade.js"; import RecaseError from "@/utils/errorUtils.js"; -import { StatusCodes } from "http-status-codes"; +import { handleProratedDowngrade } from "./arrearProratedUsage/handleProratedDowngrade.js"; +import { handleProratedUpgrade } from "./arrearProratedUsage/handleProratedUpgrade.js"; export const getUsageFromBalance = ({ ent, @@ -37,18 +33,18 @@ export const getUsageFromBalance = ({ price: Price; balance: number; }) => { - let config = price.config as UsagePriceConfig; - let billingUnits = config.billing_units || 1; + const config = price.config as UsagePriceConfig; + const billingUnits = config.billing_units || 1; // Should get overage... - let overage = -Math.min(0, balance); - let roundedOverage = new Decimal(overage) + const overage = -Math.min(0, balance); + const roundedOverage = new Decimal(overage) .div(billingUnits) .ceil() .mul(billingUnits) .toNumber(); - let usage = new Decimal(ent.allowance!).sub(balance).toNumber(); + const usage = new Decimal(ent.allowance!).sub(balance).toNumber(); let roundedUsage = usage; if (overage > 0) { @@ -90,9 +86,9 @@ export const adjustAllowance = async ({ logger: any; errorIfIncomplete?: boolean; }) => { - let cusPrice = getRelatedCusPrice(cusEnt, cusPrices); - let billingType = cusPrice ? getBillingType(cusPrice.price.config!) : null; - let cusProduct = cusEnt.customer_product; + const cusPrice = getRelatedCusPrice(cusEnt, cusPrices); + const billingType = cusPrice ? getBillingType(cusPrice.price.config!) : null; + const cusProduct = cusEnt.customer_product; // TODO: TRACK @@ -105,7 +101,7 @@ export const adjustAllowance = async ({ return { newReplaceables: [], invoice: null, deletedReplaceables: null }; } - let ent = cusEnt.entitlement; + const ent = cusEnt.entitlement; if (ent.usage_limit && newBalance < ent.allowance! - (ent.usage_limit || 0)) { throw new RecaseError({ message: `Balance exceeds usage limit of ${cusEnt.entitlement.usage_limit}`, @@ -118,8 +114,8 @@ export const adjustAllowance = async ({ logger.info(`Updating arrear prorated usage: ${affectedFeature.name}`); logger.info(`Customer: ${customer.name}, Org: ${org.slug}`); - let stripeCli = createStripeCli({ org, env }); - let sub = await getUsageBasedSub({ + const stripeCli = createStripeCli({ org, env }); + const sub = await getUsageBasedSub({ db, stripeCli, subIds: cusProduct.subscription_ids!, @@ -131,7 +127,7 @@ export const adjustAllowance = async ({ return { newReplaceables: null, invoice: null, deletedReplaceables: null }; } - let subItem = findStripeItemForPrice({ + const subItem = findStripeItemForPrice({ price: cusPrice.price, stripeItems: sub.items.data, }); @@ -141,7 +137,7 @@ export const adjustAllowance = async ({ return { newReplaceables: null, invoice: null, deletedReplaceables: null }; } - let isUpgrade = newBalance < originalBalance; + const isUpgrade = newBalance < originalBalance; if (isUpgrade) { return await handleProratedUpgrade({ diff --git a/server/src/utils/auth.ts b/server/src/utils/auth.ts index 185a10cd4..3e000aa2c 100644 --- a/server/src/utils/auth.ts +++ b/server/src/utils/auth.ts @@ -135,8 +135,8 @@ export const auth = betterAuth({ organizationCreation: { disabled: false, - afterCreate: async ({ organization }) => { - await afterOrgCreated({ org: organization as any }); + afterCreate: async ({ organization, user }) => { + await afterOrgCreated({ org: organization, user }); }, }, }), diff --git a/server/src/utils/authUtils/afterOrgCreated.ts b/server/src/utils/authUtils/afterOrgCreated.ts index 4f3a1a546..aa3f7a6ca 100644 --- a/server/src/utils/authUtils/afterOrgCreated.ts +++ b/server/src/utils/authUtils/afterOrgCreated.ts @@ -1,10 +1,12 @@ -import { db } from "@/db/initDrizzle.js"; -import { OrgService } from "@/internal/orgs/OrgService.js"; import { AppEnv } from "@autumn/shared"; -import { generatePublishableKey } from "../encryptUtils.js"; -import { createSvixApp } from "@/external/svix/svixHelpers.js"; +import type { User } from "better-auth"; +import type { Organization } from "better-auth/plugins"; +import { db } from "@/db/initDrizzle.js"; import { logger } from "@/external/logtail/logtailUtils.js"; -import { Organization } from "better-auth/plugins"; +import { createSvixApp } from "@/external/svix/svixHelpers.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; +import { createConnectAccount } from "@/internal/orgs/orgUtils/createConnectAccount.js"; +import { generatePublishableKey } from "../encryptUtils.js"; export const initOrgSvixApps = async ({ id, @@ -34,9 +36,16 @@ export const initOrgSvixApps = async ({ return { sandboxApp, liveApp }; }; -export const afterOrgCreated = async ({ org }: { org: Organization }) => { +export const afterOrgCreated = async ({ + org, + user, + createStripeAccount = true, +}: { + org: Organization; + user: User; + createStripeAccount?: boolean; +}) => { logger.info(`Org created: ${org.id} (${org.slug})`); - const { id, slug, createdAt } = org; try { @@ -48,6 +57,26 @@ export const afterOrgCreated = async ({ org }: { org: Organization }) => { }, }); + // 1. Add stripe connect config + if (createStripeAccount) { + console.log("Creating stripe connect account"); + const stripeConnectAccount = await createConnectAccount({ + org: org, + user, + }); + + await OrgService.update({ + db, + orgId: org.id, + updates: { + default_currency: "usd", + test_stripe_connect: { + default_account_id: stripeConnectAccount.id, + }, + }, + }); + } + // 1. Create svix webhoooks const { sandboxApp, liveApp } = await initOrgSvixApps({ slug, @@ -68,8 +97,10 @@ export const afterOrgCreated = async ({ org }: { org: Organization }) => { }); logger.info(`Initialized resources for org ${id} (${slug})`); + + // biome-ignore lint/suspicious/noExplicitAny: fine } catch (error: any) { - if (error?.data && error.data.code == "23505") { + if (error?.data && error.data.code === ("23505" as string)) { logger.error( `Org ${id} already exists in Supabase -- skipping creationg`, ); diff --git a/server/src/utils/authUtils/beforeSessionCreated.ts b/server/src/utils/authUtils/beforeSessionCreated.ts index 45730beea..7c3b60323 100644 --- a/server/src/utils/authUtils/beforeSessionCreated.ts +++ b/server/src/utils/authUtils/beforeSessionCreated.ts @@ -1,22 +1,18 @@ +import { member } from "@autumn/shared"; +import type { Session } from "better-auth"; +import { eq } from "drizzle-orm"; import { db } from "@/db/initDrizzle.js"; -import { Session } from "better-auth"; -import { member, session as sessionTable } from "@autumn/shared"; -import { eq, desc } from "drizzle-orm"; import { createDefaultOrg } from "@/utils/authUtils/createDefaultOrg.js"; export const beforeSessionCreated = async (session: Session) => { try { console.log(`Running beforeSessionCreated for user ${session.userId}`); - let membership = await db.query.member.findFirst({ + const membership = await db.query.member.findFirst({ where: eq(member.userId, session.userId), }); if (membership) { - console.log( - "Returning session with active org ID:", - membership.organizationId, - ); return { data: { ...session, diff --git a/server/src/utils/authUtils/createDefaultOrg.ts b/server/src/utils/authUtils/createDefaultOrg.ts index fcad35102..97ec7e393 100644 --- a/server/src/utils/authUtils/createDefaultOrg.ts +++ b/server/src/utils/authUtils/createDefaultOrg.ts @@ -1,10 +1,10 @@ +import { invitation, user as userTable } from "@autumn/shared"; +import type { Session } from "better-auth"; +import type { Organization } from "better-auth/plugins/organization"; +import { and, eq, gt } from "drizzle-orm"; import { db } from "@/db/initDrizzle.js"; import { auth } from "@/utils/auth.js"; -import { Session } from "better-auth"; -import { and, eq, gt } from "drizzle-orm"; -import { invitation, user as userTable } from "@autumn/shared"; import { slugify } from "@/utils/genUtils.js"; -import { Organization } from "better-auth/plugins/organization"; export const createDefaultOrg = async ({ session, diff --git a/server/src/utils/constants.ts b/server/src/utils/constants.ts index 1ff42dc33..4c1471e68 100644 --- a/server/src/utils/constants.ts +++ b/server/src/utils/constants.ts @@ -23,3 +23,18 @@ export const dashboardOrigins = [ "https://staging.useautumn.com", process.env.CLIENT_URL!, ]; + +export const WEBHOOK_EVENTS = [ + "checkout.session.completed", + "customer.subscription.created", + "customer.subscription.updated", + "customer.subscription.deleted", + "customer.discount.deleted", + "invoice.paid", + "invoice.upcoming", + "invoice.created", + "invoice.finalized", + "invoice.updated", + "subscription_schedule.canceled", + "subscription_schedule.updated", +]; diff --git a/server/src/utils/errorUtils.ts b/server/src/utils/errorUtils.ts index 2a3105d20..b282b3240 100644 --- a/server/src/utils/errorUtils.ts +++ b/server/src/utils/errorUtils.ts @@ -103,7 +103,7 @@ export const handleRequestError = ({ action: string; }) => { try { - const logger = req.logtail; + const logger = req.logger; if (error instanceof RecaseError) { logger.warn( `RECASE WARNING (${req.org?.slug || "unknown"}): ${error.message} [${error.code}]`, diff --git a/server/src/utils/initUtils.ts b/server/src/utils/initUtils.ts index f7c92b9d6..50c4c2108 100644 --- a/server/src/utils/initUtils.ts +++ b/server/src/utils/initUtils.ts @@ -30,13 +30,6 @@ export const checkEnvVars = () => { ); } - if ( - !process.env.LOGTAIL_SOURCE_TOKEN || - !process.env.LOGTAIL_INGESTING_HOST - ) { - logger.warn("LOGTAIL ENV VARs not found, skipping logtail"); - } - if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_KEY) { logger.warn( `SUPABASE_URL or SUPABASE_SERVICE_KEY is not set, some actions will be skipped`, diff --git a/server/src/utils/models/Request.ts b/server/src/utils/models/Request.ts index aee54985b..2e8d5cd01 100644 --- a/server/src/utils/models/Request.ts +++ b/server/src/utils/models/Request.ts @@ -6,13 +6,13 @@ import type { Organization, } from "@autumn/shared"; import type { ClickHouseClient } from "@clickhouse/client"; -import type { Logtail } from "@logtail/node"; import type { Request as ExpressRequest, Response as ExpressResponse, } from "express"; import type { PostHog } from "posthog-node"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { Logger } from "@/external/logtail/logtailUtils.js"; export interface ExtendedRequest extends ExpressRequest { orgId: string; @@ -20,7 +20,7 @@ export interface ExtendedRequest extends ExpressRequest { org: Organization; features: Feature[]; db: DrizzleCli; - logtail: Logtail; + logtail: Logger; logger: any; clickhouseClient: ClickHouseClient; diff --git a/server/src/utils/routerUtils.ts b/server/src/utils/routerUtils.ts index bce349f46..27c3b4b40 100644 --- a/server/src/utils/routerUtils.ts +++ b/server/src/utils/routerUtils.ts @@ -265,9 +265,7 @@ export const routeHandler = async ({ } catch (error) { if (error instanceof RecaseError) { if (error.code === ErrCode.EntityNotFound) { - req.logtail.warn( - `${error.message}, org: ${req.org?.slug || req.orgId}`, - ); + req.logger.warn(`${error.message}, org: ${req.org?.slug || req.orgId}`); return res.status(404).json({ message: error.message, code: error.code, @@ -281,7 +279,7 @@ export const routeHandler = async ({ originalUrl.includes("/exchange") && error.message.includes("Invalid API Key provided") ) { - req.logtail.warn(`Exchange router, invalid API Key provided`); + req.logger.warn(`Exchange router, invalid API Key provided`); return res.status(400).json({ message: error.message, @@ -293,7 +291,7 @@ export const routeHandler = async ({ error.message.includes("not a valid email address") || error.message.includes("email: Invalid input") ) { - req.logtail.warn(`Invalid email address`); + req.logger.warn(`Invalid email address`); return res.status(400).json({ message: error.message, code: ErrCode.InvalidRequest, @@ -304,7 +302,7 @@ export const routeHandler = async ({ originalUrl.includes("/billing_portal") && error.message.includes("Provide a configuration or create your default") ) { - req.logtail.warn(`Billing portal config error, org: ${req.org?.slug}`); + req.logger.warn(`Billing portal config error, org: ${req.org?.slug}`); return res.status(404).json({ message: error.message, code: ErrCode.InvalidRequest, @@ -317,7 +315,7 @@ export const routeHandler = async ({ "Invalid URL: An explicit scheme (such as https)", ) ) { - req.logtail.warn( + req.logger.warn( `Billing portal return_url error, org: ${req.org?.slug}, return_url: ${req.body.return_url}`, ); return res.status(400).json({ diff --git a/server/src/utils/scriptUtils/initCustomer.ts b/server/src/utils/scriptUtils/initCustomer.ts index b01e81a66..32adf595c 100644 --- a/server/src/utils/scriptUtils/initCustomer.ts +++ b/server/src/utils/scriptUtils/initCustomer.ts @@ -9,13 +9,13 @@ import type { Autumn } from "autumn-js"; import type Stripe from "stripe"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import type { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.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 { createStripeCli } from "../../external/stripe/utils.js"; export const createCusInStripe = async ({ customer, diff --git a/server/src/utils/scriptUtils/scriptUtils.ts b/server/src/utils/scriptUtils/scriptUtils.ts index b58c091f2..b9a56aedd 100644 --- a/server/src/utils/scriptUtils/scriptUtils.ts +++ b/server/src/utils/scriptUtils/scriptUtils.ts @@ -5,8 +5,8 @@ import { UTCDate } from "@date-fns/utc"; import { subHours } from "date-fns"; import type { Stripe } from "stripe"; import { db } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { createLogger } from "@/external/logtail/logtailUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; import { ProductService } from "@/internal/products/ProductService.js"; @@ -179,7 +179,6 @@ export const initScript = async ({ db, features, logger, - logtail: logger, apiVersion: new ApiVersionClass(ApiVersion.V1_2), } as unknown as ExtendedRequest; diff --git a/server/src/utils/scriptUtils/testUtils/cusTestUtils.ts b/server/src/utils/scriptUtils/testUtils/cusTestUtils.ts index 1f7220f8b..5785decde 100644 --- a/server/src/utils/scriptUtils/testUtils/cusTestUtils.ts +++ b/server/src/utils/scriptUtils/testUtils/cusTestUtils.ts @@ -1,9 +1,9 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; -import { CusService } from "@/internal/customers/CusService.js"; -import { Organization } from "@autumn/shared"; +import type { Organization } from "@autumn/shared"; import { AppEnv } from "autumn-js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { cusProductToSub } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js"; export const getCusSub = async ({ db, @@ -25,7 +25,7 @@ export const getCusSub = async ({ orgId: org.id, }); - let cusProduct = fullCus.customer_products.find( + const cusProduct = fullCus.customer_products.find( (cp) => cp.product.id == productId, ); diff --git a/server/test.sh b/server/test.sh index 24893f49c..b782ac7a4 100755 --- a/server/test.sh +++ b/server/test.sh @@ -1,75 +1,6 @@ #!/bin/bash -MOCHA_SETUP="npx mocha tests/00_setup.ts" -MOCHA_CMD="npx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts" -# Group 1 -if [ "$1" == "group1" ]; then - $MOCHA_SETUP \ - && $MOCHA_CMD \ - 'tests/attach/basic/*.ts' \ - 'tests/attach/upgrade/*.ts' \ - 'tests/attach/downgrade/*.ts' -fi - -# Group 2 -if [ "$1" == "g2" ]; then - $MOCHA_SETUP && $MOCHA_CMD \ - 'tests/attach/upgradeOld/*.ts' \ - 'tests/attach/entities/*.ts' \ - 'tests/attach/migrations/*.ts' \ - 'tests/attach/newVersion/*.ts' \ - 'tests/attach/others/*.ts' \ - 'tests/attach/updateEnts/*.ts' \ - exit 0 -fi - -if [ "$1" == "g3" ]; then - $MOCHA_SETUP \ - && $MOCHA_CMD 'tests/contUse/entities/*.ts' \ - && $MOCHA_CMD 'tests/contUse/update/*.ts'\ - && $MOCHA_CMD 'tests/contUse/track/*.ts' \ - exit 0 -fi - -if [ "$1" == "g4" ]; then - $MOCHA_SETUP && $MOCHA_CMD \ - 'tests/attach/updateQuantity/*.ts' \ - 'tests/attach/multiProduct/*.ts' \ - 'tests/advanced/multiFeature/*.ts' \ - 'tests/advanced/referrals/*.ts' \ - 'tests/advanced/coupons/*.ts' -fi - -# Group 5 - Paid referrals -if [ "$1" == "paid-referrals" ]; then - $MOCHA_SETUP && $MOCHA_CMD \ - 'tests/advanced/referrals/paid/*.ts' -fi - -# Group 4 -if [ "$1" == "g4" ]; then - $MOCHA_SETUP && $MOCHA_CMD \ - 'tests/advanced/usage/*.ts' -fi - - - -if [ "$1" == "alex-parallel" ]; then - MOCHA_PARALLEL=true npx mocha 'tests/alex/00_setup.ts' && npx mocha --parallel --timeout 10000000 \ - 'tests/alex/01_free.ts' 'tests/alex/02_pro.ts' 'tests/alex/03_premium.ts' \ - 'tests/alex/04_topups.ts' 'tests/alex/05_cancel.ts' 'tests/alex/06_switch.ts' \ - --ignore 'tests/alex/00_setup.ts' - -elif [ "$1" == "alex" ]; then - npx mocha 'tests/alex/00_setup.ts' && \ - npx mocha --timeout 10000000 \ - 'tests/alex/01_free.ts' \ - --ignore 'tests/alex/00_setup.ts' - -elif [ "$1" == "alex-custom" ]; then - FILE_TO_TEST="$2" - npx mocha --timeout 10000000 "tests/alex/$FILE_TO_TEST.ts" -elif [ "$1" == "custom" ]; then +if [ "$1" == "custom" ]; then FILE_TO_TEST="$2" ARG3="$3" if [ "$ARG3" == "setup" ]; then @@ -79,41 +10,123 @@ elif [ "$1" == "custom" ]; then else npx mocha --bail --timeout 10000000 "tests/$FILE_TO_TEST.ts" fi -else - npx mocha --timeout 10000000 'tests/00_setup.ts' && npx mocha --timeout 10000000 \ - 'tests/**/*.ts' \ - --ignore 'tests/00_setup.ts' \ - --ignore 'tests/alex/**/*.ts' -fi -# # TEST PARALLEL -# if [ "$1" == "basic-parallel" ]; then -# MOCHA_PARALLEL=true $MOCHA_SETUP \ -# && $MOCHA_CMD \ + +# MOCHA_SETUP="npx mocha tests/00_setup.ts" +# MOCHA_CMD="npx mocha --parallel --timeout 10000000 --ignore tests/00_setup.ts" + +# # Group 1 +# if [ "$1" == "group1" ]; then +# $MOCHA_SETUP \ +# && $MOCHA_CMD \ # 'tests/attach/basic/*.ts' \ # 'tests/attach/upgrade/*.ts' \ -# 'tests/attach/downgrade/*.ts' \ -# && $MOCHA_CMD \ +# 'tests/attach/downgrade/*.ts' +# fi + +# # Group 2 +# if [ "$1" == "g2" ]; then +# $MOCHA_SETUP && $MOCHA_CMD \ # 'tests/attach/upgradeOld/*.ts' \ # 'tests/attach/entities/*.ts' \ # 'tests/attach/migrations/*.ts' \ -# 'tests/attach/multiProduct/*.ts' \ # 'tests/attach/newVersion/*.ts' \ # 'tests/attach/others/*.ts' \ # 'tests/attach/updateEnts/*.ts' \ +# exit 0 +# fi + +# if [ "$1" == "g3" ]; then +# $MOCHA_SETUP \ +# && $MOCHA_CMD 'tests/contUse/entities/*.ts' \ +# && $MOCHA_CMD 'tests/contUse/update/*.ts'\ +# && $MOCHA_CMD 'tests/contUse/track/*.ts' \ +# exit 0 +# fi + +# if [ "$1" == "g4" ]; then +# $MOCHA_SETUP && $MOCHA_CMD \ # 'tests/attach/updateQuantity/*.ts' \ -# 'tests/contUse/entities/*.ts' \ -# 'tests/contUse/track/*.ts' \ -# 'tests/contUse/update/*.ts' \ -# && $MOCHA_CMD \ +# 'tests/attach/multiProduct/*.ts' \ # 'tests/advanced/multiFeature/*.ts' \ # 'tests/advanced/referrals/*.ts' \ -# 'tests/advanced/coupons/*.ts' \ +# 'tests/advanced/coupons/*.ts' +# fi + +# # Group 5 - Paid referrals +# if [ "$1" == "paid-referrals" ]; then +# $MOCHA_SETUP && $MOCHA_CMD \ +# 'tests/advanced/referrals/paid/*.ts' +# fi + +# # Group 4 +# if [ "$1" == "g4" ]; then +# $MOCHA_SETUP && $MOCHA_CMD \ +# 'tests/advanced/usage/*.ts' +# fi -# elif [ "$1" == "advanced-parallel" ]; then -# MOCHA_PARALLEL=true \ -# $MOCHA_SETUP \ -# && $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts' 'tests/advanced/coupons/*.ts'\ -# # && $MOCHA_CMD 'tests/advanced/usage/*.ts' \ -# # && $MOCHA_CMD 'tests/advanced/coupons/*.ts'\ + +# if [ "$1" == "alex-parallel" ]; then +# MOCHA_PARALLEL=true npx mocha 'tests/alex/00_setup.ts' && npx mocha --parallel --timeout 10000000 \ +# 'tests/alex/01_free.ts' 'tests/alex/02_pro.ts' 'tests/alex/03_premium.ts' \ +# 'tests/alex/04_topups.ts' 'tests/alex/05_cancel.ts' 'tests/alex/06_switch.ts' \ +# --ignore 'tests/alex/00_setup.ts' + +# elif [ "$1" == "alex" ]; then +# npx mocha 'tests/alex/00_setup.ts' && \ +# npx mocha --timeout 10000000 \ +# 'tests/alex/01_free.ts' \ +# --ignore 'tests/alex/00_setup.ts' + +# elif [ "$1" == "alex-custom" ]; then +# FILE_TO_TEST="$2" +# npx mocha --timeout 10000000 "tests/alex/$FILE_TO_TEST.ts" +# elif [ "$1" == "custom" ]; then +# FILE_TO_TEST="$2" +# 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 +# else +# npx mocha --timeout 10000000 'tests/00_setup.ts' && npx mocha --timeout 10000000 \ +# 'tests/**/*.ts' \ +# --ignore 'tests/00_setup.ts' \ +# --ignore 'tests/alex/**/*.ts' +# fi + +# # # TEST PARALLEL +# # if [ "$1" == "basic-parallel" ]; then +# # MOCHA_PARALLEL=true $MOCHA_SETUP \ +# # && $MOCHA_CMD \ +# # 'tests/attach/basic/*.ts' \ +# # 'tests/attach/upgrade/*.ts' \ +# # 'tests/attach/downgrade/*.ts' \ +# # && $MOCHA_CMD \ +# # 'tests/attach/upgradeOld/*.ts' \ +# # 'tests/attach/entities/*.ts' \ +# # 'tests/attach/migrations/*.ts' \ +# # 'tests/attach/multiProduct/*.ts' \ +# # 'tests/attach/newVersion/*.ts' \ +# # 'tests/attach/others/*.ts' \ +# # 'tests/attach/updateEnts/*.ts' \ +# # 'tests/attach/updateQuantity/*.ts' \ +# # 'tests/contUse/entities/*.ts' \ +# # 'tests/contUse/track/*.ts' \ +# # 'tests/contUse/update/*.ts' \ +# # && $MOCHA_CMD \ +# # 'tests/advanced/multiFeature/*.ts' \ +# # 'tests/advanced/referrals/*.ts' \ +# # 'tests/advanced/coupons/*.ts' \ + + +# # elif [ "$1" == "advanced-parallel" ]; then +# # MOCHA_PARALLEL=true \ +# # $MOCHA_SETUP \ +# # && $MOCHA_CMD 'tests/advanced/arrear_prorated/*.ts' 'tests/advanced/coupons/*.ts'\ +# # # && $MOCHA_CMD 'tests/advanced/usage/*.ts' \ +# # # && $MOCHA_CMD 'tests/advanced/coupons/*.ts'\ diff --git a/server/test.ts b/server/test.ts new file mode 100644 index 000000000..08c12af00 --- /dev/null +++ b/server/test.ts @@ -0,0 +1,79 @@ +import "dotenv/config"; +import Stripe from "stripe"; + +const main = async () => { + const stripe = new Stripe(process.env.STRIPE_SANDBOX_SECRET_KEY || ""); + + const result = await stripe.webhookEndpoints.create({ + url: "https://express.dev.useautumn.com/webhooks/connect/sandbox", + enabled_events: [ + "checkout.session.completed", + "customer.subscription.created", + "customer.subscription.updated", + "customer.subscription.deleted", + "customer.discount.deleted", + "invoice.paid", + "invoice.upcoming", + "invoice.created", + "invoice.finalized", + "invoice.updated", + "subscription_schedule.canceled", + "subscription_schedule.updated", + ], + connect: true, + }); + + console.log(result); + + // const account = await stripe.v2.core.accounts.create({ + // contact_email: "johnyeo10@gmail.com", + // display_name: "John Yeo", + // dashboard: "full", + // identity: { + // country: "us", + // }, + // configuration: { + // merchant: {}, + // }, + // defaults: { + // responsibilities: { + // losses_collector: "stripe", + // fees_collector: "stripe", + // }, + // }, + // }); + // console.log(account); + + // console.log(result); + + // const result = await stripe.v2.core.accounts.create({ + // contact_email: "johnyeo10@gmail.com", + // display_name: "John Yeo", + // dashboard: "full", + // identity: { + // country: "us", + // }, + // configuration: { + // merchant: {}, + // }, + // defaults: { + // responsibilities: { + // losses_collector: "stripe", + // fees_collector: "stripe", + // }, + // }, + // }); + // console.log(result); + + // const accountLink = await stripe.accountLinks.create({ + // account: "acct_1SIqs0RAB2jVVcNG", + // refresh_url: "https://useautumn.com/refresh", + // return_url: "https://useautumn.com/return", + // type: "account_onboarding", + // }); + // console.log(accountLink); +}; + +main() + .catch(console.error) + .then(() => process.exit(0)); diff --git a/server/tests/00_setup.ts b/server/tests/00_setup.ts index b015071fc..2b1549360 100644 --- a/server/tests/00_setup.ts +++ b/server/tests/00_setup.ts @@ -1,35 +1,31 @@ import dotenv from "dotenv"; + dotenv.config(); import { AppEnv } from "@autumn/shared"; import { clearOrg, setupOrg } from "tests/utils/setup.js"; +import { initDrizzle } from "@/db/initDrizzle.js"; import { - features, - products, - creditSystems, advanceProducts, attachProducts, - rewards, - oneTimeProducts, + creditSystems, entityProducts, + features, + oneTimeProducts, + products, referralPrograms, + rewards, } from "./global.js"; -import { initDrizzle } from "@/db/initDrizzle.js"; const ORG_SLUG = process.env.TESTS_ORG!; const DEFAULT_ENV = AppEnv.Sandbox; -import { Hyperbrowser } from "@hyperbrowser/sdk"; -const hyperbrowser = new Hyperbrowser({ - apiKey: process.env.HYPERBROWSER_API_KEY, -}); - describe("Initialize org for tests", () => { it("should initialize org", async function () { this.timeout(1000000000); this.org = await clearOrg({ orgSlug: ORG_SLUG, env: DEFAULT_ENV }); this.env = DEFAULT_ENV; - let { db, client } = initDrizzle(); + const { db, client } = initDrizzle(); this.db = db; this.client = client; diff --git a/server/tests/advanced/coupons/coupon2.ts b/server/tests/advanced/coupons/coupon2.ts index fca812a3e..cadd28dd8 100644 --- a/server/tests/advanced/coupons/coupon2.ts +++ b/server/tests/advanced/coupons/coupon2.ts @@ -57,7 +57,6 @@ const reward: CreateReward = { describe( chalk.yellow(`${testCase} - Testing one-off rollover, apply to usage only`), () => { - let logger: any; const customerId = testCase; let stripeCli: Stripe; let testClockId: string; @@ -67,7 +66,7 @@ describe( let env: AppEnv; let db: DrizzleCli; - let couponAmount = reward.discount_config!.discount_value; + let couponAmount = reward.discount_config?.discount_value ?? 0; before(async function () { await setupBefore(this); diff --git a/server/tests/advanced/multiFeature/multiFeature3.ts b/server/tests/advanced/multiFeature/multiFeature3.ts index 9a090ca1a..49f0a5e01 100644 --- a/server/tests/advanced/multiFeature/multiFeature3.ts +++ b/server/tests/advanced/multiFeature/multiFeature3.ts @@ -1,35 +1,34 @@ -import chalk from "chalk"; -import { expect } from "chai"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { features } from "tests/global.js"; -import { setupBefore } from "tests/before.js"; - +/** biome-ignore-all lint/suspicious/noExportsInTest: needed */ import { - AppEnv, + type AppEnv, BillingInterval, EntInterval, ProductItemFeatureType, UsageModel, } from "@autumn/shared"; -import { createProducts } from "tests/utils/productUtils.js"; -import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js"; +import { expect } from "chai"; +import chalk from "chalk"; +import { addMonths } from "date-fns"; +import { setupBefore } from "tests/before.js"; +import { features } from "tests/global.js"; import { getLifetimeFreeCusEnt, getUsageCusEnt, } from "tests/utils/cusProductUtils/cusEntSearchUtils.js"; - +import { getMainCusProduct } from "tests/utils/cusProductUtils/cusProductUtils.js"; +import { createProducts } from "tests/utils/productUtils.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructFeatureItem, constructFeaturePriceItem, } from "@/internal/products/product-items/productItemUtils.js"; import { timeout } from "@/utils/genUtils.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; -import { addMonths } from "date-fns"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js"; // Scenario 1: prepaid + pay per use monthly -> prepaid + pay per use monthly -let pro = { +const pro = { id: "multiFeature3Pro", name: "Multi Feature 3 Pro", items: { @@ -62,19 +61,19 @@ export const getLifetimeAndUsageCusEnts = async ({ env: AppEnv; featureId: string; }) => { - let mainCusProduct = await getMainCusProduct({ + const mainCusProduct = await getMainCusProduct({ customerId, db, orgId, env, }); - let lifetimeCusEnt = getLifetimeFreeCusEnt({ + const lifetimeCusEnt = getLifetimeFreeCusEnt({ cusProduct: mainCusProduct!, featureId, }); - let usageCusEnt = getUsageCusEnt({ + const usageCusEnt = getUsageCusEnt({ cusProduct: mainCusProduct!, featureId, }); @@ -86,8 +85,8 @@ export const getLifetimeAndUsageCusEnts = async ({ describe(`${chalk.yellowBright( "multi-feature/multi_feature3: Testing lifetime + pay per use, advance test clock", )}`, () => { - let autumn: AutumnInt = new AutumnInt(); - let customerId = "multiFeature3Customer"; + const autumn: AutumnInt = new AutumnInt(); + const customerId = "multiFeature3Customer"; let totalUsage = 0; @@ -95,17 +94,16 @@ describe(`${chalk.yellowBright( before(async function () { await setupBefore(this); - let { customer, testClockId: _testClockId } = - await initCustomerWithTestClock({ - customerId, - db: this.db, - org: this.org, - env: this.env, - }); + const res = await initCustomerV2({ + autumn, + customerId, + db: this.db, + org: this.org, + env: this.env, + attachPm: "success", + }); - testClockId = _testClockId; - - autumn = this.autumn; + testClockId = res.testClockId; await createProducts({ autumn, @@ -122,7 +120,7 @@ describe(`${chalk.yellowBright( product_id: pro.id, }); - let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ + const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, db: this.db, orgId: this.org.id, @@ -135,7 +133,7 @@ describe(`${chalk.yellowBright( expect(usageCusEnt?.balance).to.equal(pro.items.payPerUse.included_usage); }); - let overageValue = 30; + const overageValue = 30; it("should use lifetime allowance + overage", async function () { let value = pro.items.lifetime.included_usage as number; value += overageValue; @@ -150,7 +148,7 @@ describe(`${chalk.yellowBright( await timeout(3000); - let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ + const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, db: this.db, orgId: this.org.id, @@ -163,14 +161,14 @@ describe(`${chalk.yellowBright( }); it("cycle 1:should have correct usage after first cycle", async function () { - let advanceTo = addMonths(new Date(), 1).getTime(); + const advanceTo = addMonths(new Date(), 1).getTime(); await advanceTestClock({ stripeCli: this.stripeCli, testClockId, advanceTo, }); - let { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ + const { lifetimeCusEnt, usageCusEnt } = await getLifetimeAndUsageCusEnts({ customerId, db: this.db, orgId: this.org.id, diff --git a/server/tests/advanced/referrals/referrals2.ts b/server/tests/advanced/referrals/referrals2.ts index 3fcda1718..1aa238e1c 100644 --- a/server/tests/advanced/referrals/referrals2.ts +++ b/server/tests/advanced/referrals/referrals2.ts @@ -1,6 +1,8 @@ import { + type AppEnv, type Customer, ErrCode, + type Organization, type ReferralCode, type RewardRedemption, } from "@autumn/shared"; @@ -12,8 +14,9 @@ import { setupBefore } from "tests/before.js"; import { timeout } from "tests/utils/genUtils.js"; import { initCustomer } from "tests/utils/init.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js"; import { products, referralPrograms } from "../../global.js"; // UNCOMMENT FROM HERE @@ -29,18 +32,21 @@ describe(`${chalk.yellowBright( const redemptions: RewardRedemption[] = []; let mainCustomer: Customer; - + let org: Organization; + let env: AppEnv; before(async function () { await setupBefore(this); stripeCli = this.stripeCli; + org = this.org; + env = this.env; - const { testClockId: testClockId1, customer } = - await initCustomerWithTestClock({ - customerId: mainCustomerId, - db: this.db, - org: this.org, - env: this.env, - }); + const { testClockId: testClockId1, customer } = await initCustomerV2({ + customerId: mainCustomerId, + db: this.db, + org: this.org, + env: this.env, + autumn, + }); testClockId = testClockId1; mainCustomer = customer; @@ -95,8 +101,17 @@ describe(`${chalk.yellowBright( } // Check stripe customer - const stripeCus = (await stripeCli.customers.retrieve( + const legacyStripe = createStripeCli({ + org: org, + env: env, + legacyVersion: true, + }); + + const stripeCus = (await legacyStripe.customers.retrieve( mainCustomer.processor?.id, + { + expand: ["discount"], + }, )) as Stripe.Customer; assert.notEqual(stripeCus.discount, null); diff --git a/server/tests/advanced/referrals/referrals4.ts b/server/tests/advanced/referrals/referrals4.ts index 741d63dcd..2de7fc1a4 100644 --- a/server/tests/advanced/referrals/referrals4.ts +++ b/server/tests/advanced/referrals/referrals4.ts @@ -1,31 +1,31 @@ -import { features, products, referralPrograms } from "../../global.js"; +import type { Customer, ReferralCode, RewardRedemption } from "@autumn/shared"; import { assert } from "chai"; import chalk from "chalk"; -import { setupBefore } from "tests/before.js"; -import { Customer, ReferralCode, RewardRedemption } from "@autumn/shared"; -import { timeout } from "tests/utils/genUtils.js"; -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; -import { Stripe } from "stripe"; -import { initCustomer } from "tests/utils/init.js"; -import { compareProductEntitlements } from "tests/utils/compare.js"; import { addDays, addHours } from "date-fns"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import type { Stripe } from "stripe"; +import { setupBefore } from "tests/before.js"; +import { compareProductEntitlements } from "tests/utils/compare.js"; import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { timeout } from "tests/utils/genUtils.js"; +import { initCustomer } from "tests/utils/init.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { features, products, referralPrograms } from "../../global.js"; // UNCOMMENT FROM HERE describe(`${chalk.yellowBright( "referrals4: Testing free product referrals with trial", )}`, () => { - let mainCustomerId = "main-referral-4"; + const mainCustomerId = "main-referral-4"; // let redeemers = ["referral4-r1", "referral4-r2"]; - let redeemerId = "referral4-r1"; + const redeemerId = "referral4-r1"; let autumn: AutumnInt = new AutumnInt(); let stripeCli: Stripe; let referralCode: ReferralCode; - let redemptions: RewardRedemption[] = []; + const redemptions: RewardRedemption[] = []; let mainCustomer: Customer; let redeemer: Customer; @@ -48,7 +48,7 @@ describe(`${chalk.yellowBright( product_id: products.proWithTrial.id, }); - let { testClockId: testClockId1, customer } = + const { testClockId: testClockId1, customer } = await initCustomerWithTestClock({ customerId: redeemerId, db: this.db, @@ -60,7 +60,7 @@ describe(`${chalk.yellowBright( redeemer = customer; }); - it("should create referral code", async function () { + it("should create referral code", async () => { referralCode = await autumn.referrals.createCode({ customerId: mainCustomerId, referralId: referralPrograms.freeProduct.id, @@ -69,8 +69,8 @@ describe(`${chalk.yellowBright( assert.exists(referralCode.code); }); - it("should create redemption for each redeemer and fail if redeemed again", async function () { - let redemption: RewardRedemption = await autumn.referrals.redeem({ + it("should create redemption for each redeemer and fail if redeemed again", async () => { + const redemption: RewardRedemption = await autumn.referrals.redeem({ customerId: redeemerId, code: referralCode.code, }); @@ -78,7 +78,7 @@ describe(`${chalk.yellowBright( redemptions.push(redemption); }); - it("should not be triggered because of trial", async function () { + it("should not be triggered because of trial", async () => { await autumn.attach({ customer_id: redeemerId, product_id: products.proWithTrial.id, @@ -87,13 +87,13 @@ describe(`${chalk.yellowBright( await timeout(3000); // Get redemption object - let redemption = await autumn.redemptions.get(redemptions[0].id); + const redemption = await autumn.redemptions.get(redemptions[0].id); assert.equal(redemption.triggered, false); }); - it("should be triggered after trial ends", async function () { - let advanceTo = addHours( + it("should be triggered after trial ends", async () => { + const advanceTo = addHours( addDays(new Date(), 7), hoursToFinalizeInvoice, ).getTime(); @@ -104,7 +104,7 @@ describe(`${chalk.yellowBright( waitForSeconds: 30, }); - let redemption = await autumn.redemptions.get(redemptions[0].id); + const redemption = await autumn.redemptions.get(redemptions[0].id); assert.equal(redemption.triggered, true); diff --git a/server/tests/alex/05_cancel.ts b/server/tests/alex/05_cancel.ts index df899a8e1..06a510349 100644 --- a/server/tests/alex/05_cancel.ts +++ b/server/tests/alex/05_cancel.ts @@ -9,8 +9,8 @@ import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; import { timeout } from "tests/utils/genUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js"; import { alexProducts } from "./init.js"; diff --git a/server/tests/alex/06_switch.ts b/server/tests/alex/06_switch.ts index 91ef3a122..2341361bc 100644 --- a/server/tests/alex/06_switch.ts +++ b/server/tests/alex/06_switch.ts @@ -8,7 +8,7 @@ import { compareMainProduct } from "tests/utils/compare.js"; import { timeout } from "tests/utils/genUtils.js"; import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { initCustomerV2 } from "@/utils/scriptUtils/initCustomer.js"; import { alexProducts } from "./init.js"; diff --git a/server/tests/archives/03_cancel.ts b/server/tests/archives/03_cancel.ts index 82d1c0fdb..28bae415d 100644 --- a/server/tests/archives/03_cancel.ts +++ b/server/tests/archives/03_cancel.ts @@ -1,4 +1,3 @@ -// import { createStripeCli } from "@/external/stripe/utils.js"; // import { AutumnCli } from "../cli/AutumnCli.js"; // import { features, products } from "../global.js"; // import { initCustomer } from "../utils/init.js"; diff --git a/server/tests/archives/arrear_prorated/arrear_prorated2.ts b/server/tests/archives/arrear_prorated/arrear_prorated2.ts index 3aae3b373..8ee7f684d 100644 --- a/server/tests/archives/arrear_prorated/arrear_prorated2.ts +++ b/server/tests/archives/arrear_prorated/arrear_prorated2.ts @@ -7,7 +7,7 @@ // import { compareMainProduct } from "tests/utils/compare.js"; // import { advanceTestClock } from "tests/utils/stripeUtils.js"; // import { timeout } from "tests/utils/genUtils.js"; -// import { createStripeCli } from "@/external/stripe/utils.js"; + // import { addDays, addMonths, format } from "date-fns"; // import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; diff --git a/server/tests/archives/arrear_prorated/arrear_prorated3.ts b/server/tests/archives/arrear_prorated/arrear_prorated3.ts index ca65afdc9..80161e59b 100644 --- a/server/tests/archives/arrear_prorated/arrear_prorated3.ts +++ b/server/tests/archives/arrear_prorated/arrear_prorated3.ts @@ -1,18 +1,16 @@ import { expect } from "chai"; +import chalk from "chalk"; +import { addDays, addMonths, format } from "date-fns"; +import { Decimal } from "decimal.js"; +import type Stripe from "stripe"; import { AutumnCli } from "tests/cli/AutumnCli.js"; import { advanceProducts, features } from "tests/global.js"; import { compareMainProduct } from "tests/utils/compare.js"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { timeout } from "tests/utils/genUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { addDays, addMonths, format } from "date-fns"; -import chalk from "chalk"; -import Stripe from "stripe"; - -import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; import { checkSubscriptionContainsProducts } from "tests/utils/scheduleCheckUtils.js"; -import { Decimal } from "decimal.js"; -import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { initCustomerWithTestClock } from "tests/utils/testInitUtils.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; const advanceAPThroughBalances = async ({ stripeSub, @@ -32,32 +30,32 @@ const advanceAPThroughBalances = async ({ startingBalance?: number; }) => { // 1. Get total period - let totalPeriod = + const totalPeriod = (stripeSub.current_period_end - stripeSub.current_period_start) * 1000; // 2. Get allowance - let allowance = + const allowance = advanceProducts.proratedArrearSeats.entitlements.seats.allowance!; // 3. Get starting balance let balance = startingBalance || allowance; // 4. Get price per seat - let pricePerSeat = + const pricePerSeat = advanceProducts.proratedArrearSeats.prices[1].config.usage_tiers[0].amount; - let skipDays = 2; + const skipDays = 2; // 5. Get accrued price let accruedPrice = 0; if (startingBalance && startingBalance < 0) { - let proratedPrice = + const proratedPrice = (-startingBalance * pricePerSeat * (startingFrom! - stripeSub.current_period_start * 1000)) / totalPeriod; - let previouslyPaid = pricePerSeat * -startingBalance; - let priceToPay = proratedPrice - previouslyPaid; + const previouslyPaid = pricePerSeat * -startingBalance; + const priceToPay = proratedPrice - previouslyPaid; accruedPrice = priceToPay; // accruedPrice = Math.max(accruedPrice, 0); @@ -66,19 +64,19 @@ const advanceAPThroughBalances = async ({ } let curTime = startingFrom || stripeSub.current_period_start * 1000; - let numberOfEvents = 2; + const numberOfEvents = 2; console.group(); console.group(); for (let i = 0; i < numberOfEvents; i++) { - let sign = balance > 0 ? 1 : Math.random() > 0.7 ? 1 : -1; + const sign = balance > 0 ? 1 : Math.random() > 0.7 ? 1 : -1; - let currentUsage = allowance - balance; + const currentUsage = allowance - balance; - let nextBoundary = + const nextBoundary = Math.ceil((currentUsage + 1) / billingUnits) * billingUnits; - let prevBoundary = nextBoundary - billingUnits; + const prevBoundary = nextBoundary - billingUnits; let valueNeeded = 0; if (sign > 0) { @@ -95,8 +93,8 @@ const advanceAPThroughBalances = async ({ ); } - let newBalance = balance - valueNeeded; - let totalUsage = allowance - newBalance; + const newBalance = balance - valueNeeded; + const totalUsage = allowance - newBalance; await AutumnCli.usage({ customerId, @@ -106,19 +104,19 @@ const advanceAPThroughBalances = async ({ await timeout(2000); - let prevBalance = balance; + const prevBalance = balance; balance = newBalance; // Calculate prorated price only when crossing boundary - let newPrice = Math.max(0, -balance * pricePerSeat); - let prevCurTime = curTime; + const newPrice = Math.max(0, -balance * pricePerSeat); + const prevCurTime = curTime; curTime = addDays(curTime, 2).getTime(); if (i === numberOfEvents - 1) { curTime = stripeSub.current_period_end * 1000; } - let proratedPrice = new Decimal(newPrice) + const proratedPrice = new Decimal(newPrice) .mul(curTime - prevCurTime) .div(totalPeriod); accruedPrice = new Decimal(accruedPrice) @@ -145,8 +143,8 @@ const advanceAPThroughBalances = async ({ // Advance test clock to end of period - let advanceTo = addDays(addMonths(new Date(), 1), 2); - let advanceToStart = startingFrom ? new Date(startingFrom) : new Date(); + const advanceTo = addDays(addMonths(new Date(), 1), 2); + const advanceToStart = startingFrom ? new Date(startingFrom) : new Date(); await advanceTestClock({ stripeCli, @@ -158,12 +156,12 @@ const advanceAPThroughBalances = async ({ // Check invoice amount const res = await AutumnCli.getCustomer(customerId); - let invoice = res.invoices[0]; + const invoice = res.invoices[0]; - let basePrice = advanceProducts.proratedArrearSeats.prices[0].config.amount; - let nextMonthUsagePrice = Math.max(-balance * pricePerSeat, 0); + const basePrice = advanceProducts.proratedArrearSeats.prices[0].config.amount; + const nextMonthUsagePrice = Math.max(-balance * pricePerSeat, 0); - let expectedInvoiceTotal = Number( + const expectedInvoiceTotal = Number( (accruedPrice + basePrice + nextMonthUsagePrice).toFixed(2), ); console.log( @@ -192,7 +190,7 @@ describe(`${chalk.yellowBright( let stripeCli: Stripe; let subId = ""; let stripeSub: Stripe.Subscription; - let billingUnits = + const billingUnits = advanceProducts.proratedArrearSeats.prices[1].config.billing_units || 1; before(async function () { @@ -244,7 +242,7 @@ describe(`${chalk.yellowBright( let balance: number; it("arrear_prorated3: should run first cycles and have correct invoice / balance", async () => { // Do it again - let { advancedTo: advancedTo1, balance: balance1 } = + const { advancedTo: advancedTo1, balance: balance1 } = await advanceAPThroughBalances({ stripeSub, stripeCli, @@ -264,7 +262,7 @@ describe(`${chalk.yellowBright( ); } - let newStripeSub = await stripeCli.subscriptions.retrieve(subId); + const newStripeSub = await stripeCli.subscriptions.retrieve(subId); await advanceAPThroughBalances({ stripeSub: newStripeSub, stripeCli, diff --git a/server/tests/attach/basic/basic3.ts b/server/tests/attach/basic/basic3.ts index 78661c861..369f6c299 100644 --- a/server/tests/attach/basic/basic3.ts +++ b/server/tests/attach/basic/basic3.ts @@ -1,3 +1,4 @@ +import type { AppEnv, Organization } from "@autumn/shared"; import { expect } from "chai"; import chalk from "chalk"; import { setupBefore } from "tests/before.js"; @@ -7,6 +8,7 @@ import { compareMainProduct } from "tests/utils/compare.js"; import { timeout } from "tests/utils/genUtils.js"; import { createProducts } from "tests/utils/productUtils.js"; import { completeCheckoutForm } from "tests/utils/stripeUtils.js"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js"; import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js"; @@ -51,7 +53,7 @@ const testCase = "basic3"; describe(`${chalk.yellowBright("basic3: Testing attach one time / monthly add ons")}`, () => { const customerId = testCase; const autumn: AutumnInt = new AutumnInt(); - let db, org, env; + let db: DrizzleCli, org: Organization, env: AppEnv; before(async function () { await setupBefore(this); diff --git a/server/tests/attach/basic/basic5.ts b/server/tests/attach/basic/basic5.ts index 53e776a6c..7cc0e3827 100644 --- a/server/tests/attach/basic/basic5.ts +++ b/server/tests/attach/basic/basic5.ts @@ -1,14 +1,14 @@ -import { createStripeCli } from "@/external/stripe/utils.js"; +import { CusProductStatus } from "@autumn/shared"; +import { expect } from "chai"; +import chalk from "chalk"; +import type Stripe from "stripe"; +import { setupBefore } from "tests/before.js"; import { AutumnCli } from "tests/cli/AutumnCli.js"; import { products } from "tests/global.js"; -import chalk from "chalk"; import { compareMainProduct } from "tests/utils/compare.js"; -import { expect } from "chai"; -import { CusProductStatus } from "@autumn/shared"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -import { setupBefore } from "tests/before.js"; -import Stripe from "stripe"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { timeout } from "@/utils/genUtils.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; const testCase = "basic5"; describe(`${chalk.yellowBright( @@ -30,7 +30,7 @@ describe(`${chalk.yellowBright( }); }); - it("should attach pro product", async function () { + it("should attach pro product", async () => { await AutumnCli.attach({ customerId: customerId, productId: products.pro.id, @@ -55,7 +55,7 @@ describe(`${chalk.yellowBright( return; - it("should have pro product active, and canceled_at != null, and free scheduled", async function () { + it("should have pro product active, and canceled_at != null, and free scheduled", async () => { const cusRes: any = await AutumnCli.getCustomer(customerId); compareMainProduct({ sent: products.pro, @@ -75,7 +75,7 @@ describe(`${chalk.yellowBright( expect(freeProduct.status).to.equal(CusProductStatus.Scheduled); }); - it("should cancel pro product (now)", async function () { + it("should cancel pro product (now)", async () => { const cusRes: any = await AutumnCli.getCustomer(customerId); const proProduct = cusRes.products.find( (p: any) => p.id === products.pro.id, @@ -87,7 +87,7 @@ describe(`${chalk.yellowBright( await timeout(5000); }); - it("should have free product active, and no pro product", async function () { + it("should have free product active, and no pro product", async () => { const cusRes: any = await AutumnCli.getCustomer(customerId); compareMainProduct({ sent: products.free, diff --git a/server/tests/attach/basic/basic6.ts b/server/tests/attach/basic/basic6.ts index 4365d1635..170837382 100644 --- a/server/tests/attach/basic/basic6.ts +++ b/server/tests/attach/basic/basic6.ts @@ -1,16 +1,15 @@ -import { createStripeCli } from "@/external/stripe/utils.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; +import { CusProductStatus, type Customer } from "@autumn/shared"; +import { expect } from "chai"; import chalk from "chalk"; +import { addHours, addMonths } from "date-fns"; +import type Stripe from "stripe"; import { setupBefore } from "tests/before.js"; -import Stripe from "stripe"; -import { advanceTestClock } from "tests/utils/stripeUtils.js"; import { AutumnCli } from "tests/cli/AutumnCli.js"; import { products } from "tests/global.js"; -import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; -import { CusProductStatus, Customer } from "@autumn/shared"; -import { addHours, addMonths } from "date-fns"; import { hoursToFinalizeInvoice } from "tests/utils/constants.js"; -import { expect } from "chai"; +import { advanceTestClock } from "tests/utils/stripeUtils.js"; +import { attachFailedPaymentMethod } from "@/external/stripe/stripeCusUtils.js"; +import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; const testCase = "basic6"; describe(`${chalk.yellowBright( @@ -38,7 +37,7 @@ describe(`${chalk.yellowBright( customer = customer_; }); - it("should attach pro product and switch to failed payment method", async function () { + it("should attach pro product and switch to failed payment method", async () => { await AutumnCli.attach({ customerId: customerId, productId: products.pro.id, @@ -50,7 +49,7 @@ describe(`${chalk.yellowBright( }); }); - it("should advance to next cycle", async function () { + it("should advance to next cycle", async () => { await advanceTestClock({ stripeCli, testClockId, @@ -62,7 +61,7 @@ describe(`${chalk.yellowBright( }); }); - it("should have pro product in past due status", async function () { + it("should have pro product in past due status", async () => { const cusRes: any = await AutumnCli.getCustomer(customerId); const proProduct = cusRes.products.find( (p: any) => p.id === products.pro.id, diff --git a/server/tests/attach/downgrade/downgrade9.ts b/server/tests/attach/downgrade/downgrade9.ts index 42e42c3f4..e69de29bb 100644 --- a/server/tests/attach/downgrade/downgrade9.ts +++ b/server/tests/attach/downgrade/downgrade9.ts @@ -1,116 +0,0 @@ -import chalk from "chalk"; -import { AutumnCli } from "tests/cli/AutumnCli.js"; -import { advanceProducts } from "tests/global.js"; -import { - checkProductIsScheduled, - compareMainProduct, -} from "tests/utils/compare.js"; -import { setupBefore } from "tests/before.js"; -import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; - -// TEST MULTI INTERVAL DOWNGRADE -// -/* -CASE 1: Annual pro -> Annual starter - - If attach annual starter, should schedule correctly [DONE] - - If advance test clock, should downgrade correctly (to monthly starter) [DONE] - - If cancel active subscription (on Stripe), should remove scheduled correctly [DONE] - - If cancel scheduled subscription (on Stripe), should remove scheduled correctly [DONE] - - If expire on dashboard, should remove scheduled correctly - - - If upgrade back to annual pro, should remove scheduled correctly [DONE] - - If downgrade to monthly pro (switch downgrade), should be correct [DONE] - - If downgrade to free (switch downgrade), should be correct -*/ - -const testCase = "downgrade9"; - -describe(`${chalk.yellowBright("downgrade9: Multi interval downgrade -- Annual pro -> Annual starter")}`, () => { - let customerId = testCase; - - before(async function () { - await setupBefore(this); - await initCustomer({ - customerId, - db: this.db, - org: this.org, - env: this.env, - autumn: this.autumnJs, - attachPm: "success", - withTestClock: false, - }); - }); - - it("should attach annual pro", async function () { - await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuProAnnual.id, - }); - - let cusRes = await AutumnCli.getCustomer(customerId); - compareMainProduct({ - sent: advanceProducts.gpuProAnnual, - cusRes, - }); - }); - - it("should attach downgrade to annual starter", async function () { - await AutumnCli.attach({ - customerId: customerId, - productId: advanceProducts.gpuStarterAnnual.id, - }); - - let cusRes = await AutumnCli.getCustomer(customerId); - checkProductIsScheduled({ - cusRes, - product: advanceProducts.gpuStarterAnnual, - }); - }); -}); - -// describe(`${chalk.yellowBright("downgrade9: Multi interval downgrade -- Quarterly pro -> Monthly pro")}`, () => { -// let customerId = testCase; -// let stripeCli: Stripe; -// let testClockId: string; - -// before(async function () { -// const { testClockId: insertedTestClockId } = -// await initCustomerWithTestClock({ -// customerId, -// org: this.org, -// env: this.env, -// db: this.db, -// }); -// testClockId = insertedTestClockId; -// stripeCli = createStripeCli({ -// org: this.org, -// env: this.env, -// }); -// }); - -// it("should attach quarterly pro", async function () { -// let res = await AutumnCli.attach({ -// customerId: customerId, -// productId: advanceProducts.gpuProQuarter.id, -// }); - -// let cusRes = await AutumnCli.getCustomer(customerId); -// compareMainProduct({ -// sent: advanceProducts.gpuProQuarter, -// cusRes, -// }); -// }); - -// it("should attach downgrade to monthly pro", async function () { -// let res = await AutumnCli.attach({ -// customerId: customerId, -// productId: advanceProducts.gpuSystemPro.id, -// }); - -// let cusRes = await AutumnCli.getCustomer(customerId); -// checkProductIsScheduled({ -// cusRes, -// product: advanceProducts.gpuSystemPro, -// }); -// }); -// }); diff --git a/server/tests/attach/multiProduct/multiProduct3.ts b/server/tests/attach/multiProduct/multiProduct3.ts index f8e15cdff..e69de29bb 100644 --- a/server/tests/attach/multiProduct/multiProduct3.ts +++ b/server/tests/attach/multiProduct/multiProduct3.ts @@ -1,155 +0,0 @@ -// import chalk from "chalk"; - -// import { expect } from "chai"; -// import { AutumnCli } from "tests/cli/AutumnCli.js"; -// import { attachProducts } from "tests/global.js"; -// import { compareMainProduct } from "tests/utils/compare.js"; -// import { searchCusProducts, timeout } from "tests/utils/genUtils.js"; - -// import { createStripeCli } from "@/external/stripe/utils.js"; -// import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js"; -// import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js"; -// import { initCustomer } from "@/utils/scriptUtils/initCustomer.js"; -// import { setupBefore } from "tests/before.js"; -// import Stripe from "stripe"; - -// // TESTING DOWNGRADE DOWNGRADE THEN -// // 1. UPGRADE FIRST PRODUCT BACK -- SHOULD REPLACE SCHEDULE WITH OLD FIRST PRODUCT -// // 2. UPGRADE SECOND PRODUCT BACK -- SHOULD CANCEL SCHEDULE - -// const testCase = "multiProduct3"; -// describe( -// chalk.yellowBright(`${testCase}: double downgrade, double upgrade (back)`), -// () => { -// let customerId = testCase; -// let customer; -// let stripeCli: Stripe; - -// before(async function () { -// await setupBefore(this); -// stripeCli = this.stripeCli; -// const res = await initCustomer({ -// db: this.db, -// org: this.org, -// env: this.env, -// customerId, -// autumn: this.autumnJs, -// attachPm: "success", -// }); - -// customer = res.customer; -// }); - -// it("should attach premium group 1 and premium group 2", async function () { -// await AutumnCli.attach({ -// customerId: customerId, -// productIds: [ -// attachProducts.premiumGroup1.id, -// attachProducts.premiumGroup2.id, -// ], -// }); - -// let cusRes = await AutumnCli.getCustomer(customerId); -// compareMainProduct({ sent: attachProducts.premiumGroup1, cusRes }); -// compareMainProduct({ sent: attachProducts.premiumGroup2, cusRes }); -// }); - -// it("should attach starter group 1, then starter group 2", async function () { -// await AutumnCli.attach({ -// customerId: customerId, -// productId: attachProducts.starterGroup1.id, -// }); - -// await AutumnCli.attach({ -// customerId: customerId, -// productId: attachProducts.starterGroup2.id, -// }); -// }); - -// it("should reattach premium group 1", async function () { -// await AutumnCli.attach({ -// customerId: customerId, -// productId: attachProducts.premiumGroup1.id, -// }); - -// await timeout(10000); - -// const cusProducts = await CusProductService.list({ -// db: this.db, -// internalCustomerId: customer!.internal_id, -// }); - -// let premiumGroup1 = searchCusProducts({ -// cusProducts, -// productId: attachProducts.premiumGroup1.id, -// }); - -// let starterGroup2 = searchCusProducts({ -// cusProducts, -// productId: attachProducts.starterGroup2.id, -// }); - -// expect(premiumGroup1!.scheduled_ids!.length).to.equal(1); -// expect(starterGroup2!.scheduled_ids!.length).to.equal(1); -// expect(premiumGroup1!.scheduled_ids![0]).to.equal( -// starterGroup2!.scheduled_ids![0] -// ); - -// // 2. Check that there's no starter group 1 -// let starterGroup1 = searchCusProducts({ -// cusProducts, -// productId: attachProducts.starterGroup1.id, -// }); - -// expect(starterGroup1).to.not.exist; - -// // 3. TODO: check that in Stripe schedule, premium group 1 and starter group 2 are scheduled -// }); - -// it("should reattach premium group 2 (scheduled should be cancelled)", async function () { -// await timeout(3000); -// let res = await AutumnCli.attach({ -// customerId: customerId, -// productId: attachProducts.premiumGroup2.id, -// }); - -// await timeout(10000); - -// const cusProducts = await CusProductService.list({ -// db: this.db, -// internalCustomerId: customer!.internal_id, -// }); - -// let premiumGroup2 = searchCusProducts({ -// cusProducts, -// productId: attachProducts.premiumGroup2.id, -// }); - -// let premiumGroup1 = searchCusProducts({ -// cusProducts, -// productId: attachProducts.premiumGroup1.id, -// }); - -// expect(premiumGroup2).to.exist.and.have.property("scheduled_ids"); -// expect(premiumGroup1).to.exist.and.have.property("scheduled_ids"); -// expect(premiumGroup2!.scheduled_ids!.length).to.equal(0); -// expect(premiumGroup1!.scheduled_ids!.length).to.equal(0); - -// // Check that subscription is activated -// let stripeCli = createStripeCli({ -// org: this.org, -// env: this.env, -// }); - -// let subs = await getStripeSubs({ -// stripeCli, -// subIds: premiumGroup1!.subscription_ids!, -// }); - -// let sub = subs[0]; -// expect(sub.canceled_at).to.equal(null); -// expect(sub.cancel_at).to.equal(null); -// expect(sub.status).to.equal("active"); -// }); -// } -// ); diff --git a/server/tests/before.ts b/server/tests/before.ts index 71450dab3..9b69ce053 100644 --- a/server/tests/before.ts +++ b/server/tests/before.ts @@ -1,23 +1,18 @@ import dotenv from "dotenv"; + dotenv.config(); -import { Autumn as AutumnJS } from "autumn-js"; -import { createSupabaseClient } from "@/external/supabaseUtils.js"; import { AppEnv } from "@autumn/shared"; -import { OrgService } from "@/internal/orgs/OrgService.js"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { initDrizzle } from "@/db/initDrizzle.js"; +import { Autumn as AutumnJS } from "autumn-js"; import { after } from "mocha"; +import { initDrizzle } from "@/db/initDrizzle.js"; +import { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; const ORG_SLUG = process.env.TESTS_ORG!; const DEFAULT_ENV = AppEnv.Sandbox; -import { Hyperbrowser } from "@hyperbrowser/sdk"; -const hyperbrowser = new Hyperbrowser({ - apiKey: process.env.HYPERBROWSER_API_KEY, -}); - export const setupBefore = async (instance: any) => { try { const { db, client } = initDrizzle(); diff --git a/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts b/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts index bc0eb0c6d..f2a4cd0c9 100644 --- a/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts +++ b/server/tests/core/multiAttach/multiInvoice/multiInvoice1.test.ts @@ -38,19 +38,6 @@ const pro = constructProduct({ trial: true, }); -const ops = [ - { - entityId: "1", - product: pro, - results: [{ product: pro, status: CusProductStatus.Active }], - }, - { - entityId: "2", - product: pro, - results: [{ product: pro, status: CusProductStatus.Active }], - }, -]; - const testCase = "multiInvoice1"; describe(`${chalk.yellowBright("multiInvoice1: Testing multi attach through invoice flow")}`, () => { const customerId = testCase; @@ -98,7 +85,7 @@ describe(`${chalk.yellowBright("multiInvoice1: Testing multi attach through invo testClockId = testClockId1!; }); - it("should run multi attach through checkout and have correct sub", async () => { + it("should run multi attach through invoice checkout flow", async () => { const productsList = [ { product_id: pro.id, diff --git a/server/tests/merged/mergeUtils.test.ts b/server/tests/merged/mergeUtils.test.ts index e1ef46b7f..c76244523 100644 --- a/server/tests/merged/mergeUtils.test.ts +++ b/server/tests/merged/mergeUtils.test.ts @@ -9,8 +9,8 @@ import { } from "@autumn/shared"; import { expect } from "chai"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { priceToStripeItem } from "@/external/stripe/priceToStripeItem/priceToStripeItem.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { CusService } from "@/internal/customers/CusService.js"; import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js"; import { diff --git a/server/tests/merged/mergeUtils/expectSubCorrect.ts b/server/tests/merged/mergeUtils/expectSubCorrect.ts index b3b0660ae..b79838341 100644 --- a/server/tests/merged/mergeUtils/expectSubCorrect.ts +++ b/server/tests/merged/mergeUtils/expectSubCorrect.ts @@ -12,9 +12,9 @@ import { expect } from "chai"; import type Stripe from "stripe"; import { defaultApiVersion } from "tests/constants.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { priceToStripeItem } from "@/external/stripe/priceToStripeItem/priceToStripeItem.js"; import { subIsCanceled } from "@/external/stripe/stripeSubUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { cusProductInPhase, logPhaseItems, diff --git a/server/tests/utils/expectUtils/expectMultiAttach.ts b/server/tests/utils/expectUtils/expectMultiAttach.ts index 855e85c4e..f8280d20c 100644 --- a/server/tests/utils/expectUtils/expectMultiAttach.ts +++ b/server/tests/utils/expectUtils/expectMultiAttach.ts @@ -6,6 +6,7 @@ import { type ProductOptions, type ProductV2, } from "@autumn/shared"; +import type { Customer, Entity } from "autumn-js"; import { expect } from "chai"; import { expectSubToBeCorrect } from "tests/merged/mergeUtils/expectSubCorrect.js"; import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js"; @@ -75,8 +76,10 @@ export const expectMultiAttachCorrect = async ({ await timeout(5000); } + await timeout(2500); + for (const result of results) { - let customer; + let customer: Customer | Entity; if (result.entityId) { customer = await autumn.entities.get(customerId, result.entityId); } else { @@ -84,7 +87,7 @@ export const expectMultiAttachCorrect = async ({ } expectProductAttached({ - customer, + customer: customer as Customer, product: result.product, status: result.status, entityId: result.entityId, diff --git a/server/tests/utils/scheduleCheckUtils.ts b/server/tests/utils/scheduleCheckUtils.ts index 76474efcd..379333ff9 100644 --- a/server/tests/utils/scheduleCheckUtils.ts +++ b/server/tests/utils/scheduleCheckUtils.ts @@ -1,9 +1,9 @@ -import { ProductService } from "@/internal/products/ProductService.js"; -import { AppEnv, Organization } from "@autumn/shared"; +import type { AppEnv, Organization } from "@autumn/shared"; import { expect } from "chai"; -import Stripe from "stripe"; -import { createStripeCli } from "@/external/stripe/utils.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import { ProductService } from "@/internal/products/ProductService.js"; export const checkScheduleContainsProducts = async ({ db, @@ -31,7 +31,7 @@ export const checkScheduleContainsProducts = async ({ let priceCount = 0; for (const productId of productIds) { - let product = await ProductService.getFull({ + const product = await ProductService.getFull({ db, idOrInternalId: productId, orgId: org.id, @@ -69,7 +69,7 @@ export const checkSubscriptionContainsProducts = async ({ let totalPriceCount = 0; for (const productId of productIds) { - let product = await ProductService.getFull({ + const product = await ProductService.getFull({ db, idOrInternalId: productId, orgId: org.id, diff --git a/server/tests/utils/setup.ts b/server/tests/utils/setup.ts index 8e10da299..edadaff19 100644 --- a/server/tests/utils/setup.ts +++ b/server/tests/utils/setup.ts @@ -18,8 +18,8 @@ import { initDrizzle } from "@/db/initDrizzle.js"; import { AutumnInt } from "@/external/autumn/autumnCli.js"; import { CacheManager } from "@/external/caching/CacheManager.js"; import { CacheType } from "@/external/caching/cacheActions.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { deactivateStripeMeters } from "@/external/stripe/stripeProductUtils.js"; -import { createStripeCli } from "@/external/stripe/utils.js"; import { CusService } from "@/internal/customers/CusService.js"; import { hashApiKey } from "@/internal/dev/api-keys/apiKeyUtils.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; diff --git a/server/tests/utils/testAttachUtils/trialAttachUtils.ts b/server/tests/utils/testAttachUtils/trialAttachUtils.ts index 8b475297d..768bb06f8 100644 --- a/server/tests/utils/testAttachUtils/trialAttachUtils.ts +++ b/server/tests/utils/testAttachUtils/trialAttachUtils.ts @@ -1,14 +1,14 @@ +import { type AppEnv, ProcessorType } from "@autumn/shared"; +import type Stripe from "stripe"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { AutumnInt } from "@/external/autumn/autumnCli.js"; +import { attachPmToCus } from "@/external/stripe/stripeCusUtils.js"; +import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js"; +import { newCusToAttachParams } from "@/internal/customers/attach/attachUtils/attachParams/convertToParams.js"; import { CusService } from "@/internal/customers/CusService.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { isDefaultTrialFullProduct } from "@/internal/products/productUtils/classifyProduct.js"; -import { AppEnv, ProcessorType } from "@autumn/shared"; import { generateId } from "@/utils/genUtils.js"; -import { newCusToAttachParams } from "@/internal/customers/attach/attachUtils/attachParams/convertToParams.js"; -import { handleAddProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/handleAddProduct.js"; -import Stripe from "stripe"; -import { AutumnInt } from "@/external/autumn/autumnCli.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { attachPmToCus } from "@/external/stripe/stripeCusUtils.js"; export async function manuallyAttachDefaultTrial({ customerId, @@ -133,7 +133,6 @@ export async function manuallyAttachDefaultTrial({ org, env, orgId: org.id, - logtail: console, logger: console, } as any; @@ -195,7 +194,7 @@ export async function flipDefaultState({ state: boolean; }) { try { - let productExists = await autumn.products.get(id); + const productExists = await autumn.products.get(id); if (productExists) { await autumn.products.update(id, { is_default: state, @@ -213,7 +212,7 @@ export async function flipDefaultStates({ currentCase: number; autumn: AutumnInt; }) { - let total = 4; + const total = 4; // Now flip all products from 0 to total-1, only current case should be true for (let i = 0; i < total; i++) { diff --git a/server/tests/utils/testInitUtils.ts b/server/tests/utils/testInitUtils.ts index c35b46b4c..bcec7b621 100644 --- a/server/tests/utils/testInitUtils.ts +++ b/server/tests/utils/testInitUtils.ts @@ -1,8 +1,7 @@ -import { createStripeCli } from "@/external/stripe/utils.js"; -import { AppEnv, Organization } from "@autumn/shared"; -import { SupabaseClient } from "@supabase/supabase-js"; +import type { AppEnv, Organization } from "@autumn/shared"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import { createStripeCli } from "@/external/connect/createStripeCli.js"; import { initCustomer } from "./init.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; export const initCustomerWithTestClock = async ({ customerId, @@ -22,7 +21,7 @@ export const initCustomerWithTestClock = async ({ frozen_time: Math.floor(Date.now() / 1000), }); - let customer = await initCustomer({ + const customer = await initCustomer({ customer_data: { id: customerId, name: customerId, diff --git a/shared/api/apiUtils.ts b/shared/api/apiUtils.ts new file mode 100644 index 000000000..a1162889d --- /dev/null +++ b/shared/api/apiUtils.ts @@ -0,0 +1 @@ +export * from "./common/queryHelpers.js"; diff --git a/shared/api/common/queryHelpers.ts b/shared/api/common/queryHelpers.ts index 07d21530b..89b73d857 100644 --- a/shared/api/common/queryHelpers.ts +++ b/shared/api/common/queryHelpers.ts @@ -27,3 +27,48 @@ export function queryStringArray(schema: T) { return val; }, z.array(schema)); } + +/** + * Helper to handle query string integers that come in as strings and need to be converted to numbers. + * Query parameters are always strings, so this helper parses them to integers for validation. + * + * @example + * ```ts + * const schema = z.object({ + * limit: queryInteger({ min: 1, max: 100 }).default(10), + * offset: queryInteger({ min: 0 }).default(0), + * }); + * ``` + */ +export function queryInteger(options?: { + min?: number; + max?: number; + error?: string; +}) { + let schema = z.number().int({ message: options?.error }); + + if (options?.min !== undefined) { + schema = schema.min(options.min, { + message: options?.error || `must be at least ${options.min}`, + }); + } + + if (options?.max !== undefined) { + schema = schema.max(options.max, { + message: options?.error || `must be at most ${options.max}`, + }); + } + + return z.preprocess((val) => { + // If already a number, return as-is + if (typeof val === "number") { + return val; + } + // Parse string to integer + if (typeof val === "string") { + const parsed = Number.parseInt(val, 10); + return Number.isNaN(parsed) ? val : parsed; + } + return val; + }, schema); +} diff --git a/shared/api/errors/classes/cusErrClasses.ts b/shared/api/errors/classes/cusErrClasses.ts new file mode 100644 index 000000000..b1ea172ac --- /dev/null +++ b/shared/api/errors/classes/cusErrClasses.ts @@ -0,0 +1,16 @@ +import { RecaseError } from "../base/RecaseError.js"; +import { CusErrorCode } from "../codes/cusErrCodes.js"; + +/** + * Customer not found error + */ +export class CustomerNotFoundError extends RecaseError { + constructor(opts: { customerId: string }) { + super({ + message: `Customer ${opts.customerId} not found`, + code: CusErrorCode.CustomerNotFound, + statusCode: 404, + }); + this.name = "CustomerNotFoundError"; + } +} diff --git a/shared/api/errors/classes/productErrClasses.ts b/shared/api/errors/classes/productErrClasses.ts index 008014ed8..4b3517fbc 100644 --- a/shared/api/errors/classes/productErrClasses.ts +++ b/shared/api/errors/classes/productErrClasses.ts @@ -5,7 +5,7 @@ import { ProductErrorCode } from "../codes/productErrCodes.js"; * Product not found error */ export class ProductNotFoundError extends RecaseError { - constructor(opts: { productId: string; version?: string }) { + constructor(opts: { productId: string; version?: string | number }) { super({ message: `Product ${opts.productId} ${opts.version ? ` (version ${opts.version})` : ""} not found`, code: ProductErrorCode.ProductNotFound, diff --git a/shared/api/errors/codes/cusErrCodes.ts b/shared/api/errors/codes/cusErrCodes.ts new file mode 100644 index 000000000..7c4b12cd1 --- /dev/null +++ b/shared/api/errors/codes/cusErrCodes.ts @@ -0,0 +1,8 @@ +/** + * Customer-related error codes + */ +export const CusErrorCode = { + CustomerNotFound: "customer_not_found", +} as const; + +export type CusErrorCode = (typeof CusErrorCode)[keyof typeof CusErrorCode]; diff --git a/shared/api/errors/index.ts b/shared/api/errors/index.ts index 494cfae0c..d8cd5c635 100644 --- a/shared/api/errors/index.ts +++ b/shared/api/errors/index.ts @@ -1,8 +1,8 @@ export * from "./base/InternalError.js"; export * from "./base/RecaseError.js"; - +export * from "./classes/cusErrClasses.js"; export * from "./classes/cusProductErrClasses.js"; export * from "./classes/productErrClasses.js"; - +export * from "./codes/cusErrCodes.js"; export * from "./codes/cusProductErrCodes.js"; export * from "./codes/productErrCodes.js"; diff --git a/shared/api/models.ts b/shared/api/models.ts index d5a04ec7e..0f1124626 100644 --- a/shared/api/models.ts +++ b/shared/api/models.ts @@ -54,9 +54,13 @@ export * from "./referrals/apiReferralCode.js"; export * from "./referrals/referralOpModels.js"; export * from "./referrals/referralsOpenApi.js"; +// NOTE: productsOpenApi.js is NOT exported here - it's only imported by openapi.ts for spec generation + // export * from "./products/ApiFreeTrial.js"; // export * from "./products/apiProduct.js"; // export * from "./products/apiProductItem.js"; // Errors export * from "./errors/index.js"; +// Models +export * from "./platform/platformModels.js"; diff --git a/shared/api/platform/platformModels.ts b/shared/api/platform/platformModels.ts index d6d658047..a5970c591 100644 --- a/shared/api/platform/platformModels.ts +++ b/shared/api/platform/platformModels.ts @@ -1,22 +1,13 @@ -import { queryStringArray } from "@api/common/queryHelpers.js"; +import { queryInteger, queryStringArray } from "@api/common/queryHelpers.js"; import { z } from "zod/v4"; /** * Query params for GET /platform/users endpoint */ export const ListPlatformUsersQuerySchema = z.object({ - limit: z - .number() - .int({ error: "limit must be an integer" }) - .min(1, { error: "limit must be at least 1" }) - .max(100, { error: "limit must be at most 100" }) - .default(10), + limit: queryInteger({ min: 1, max: 100 }).default(10), - offset: z - .number({ error: "offset must be a number" }) - .int({ error: "offset must be an integer" }) - .min(0, { error: "offset must be at least 0" }) - .default(0), + offset: queryInteger({ min: 0 }).default(0), expand: queryStringArray(z.enum(["organizations"])) .optional() @@ -72,3 +63,28 @@ export const ListPlatformUsersResponseSchema = z.object({ export type ListPlatformUsersResponse = z.infer< typeof ListPlatformUsersResponseSchema >; + +/** + * Query params for GET /platform/orgs endpoint + */ +export const ListPlatformOrgsQuerySchema = z.object({ + limit: queryInteger({ min: 1, max: 100 }).default(10), + + offset: queryInteger({ min: 0 }).default(0), +}); + +export type ListPlatformOrgsQuery = z.infer; + +/** + * Response schema for GET /platform/orgs + */ +export const ListPlatformOrgsResponseSchema = z.object({ + list: z.array(ApiPlatformOrgSchema), + total: z.number().describe("Total number of organizations returned"), + limit: z.number().describe("Limit used in the query"), + offset: z.number().describe("Offset used in the query"), +}); + +export type ListPlatformOrgsResponse = z.infer< + typeof ListPlatformOrgsResponseSchema +>; diff --git a/shared/api/products/productsOpenApi.ts b/shared/api/products/productsOpenApi.ts index 5c3899a7b..17a2e1749 100644 --- a/shared/api/products/productsOpenApi.ts +++ b/shared/api/products/productsOpenApi.ts @@ -1,9 +1,9 @@ -import { SuccessResponseSchema } from "@api/common/commonResponses.js"; import { CreateProductV2ParamsSchema, UpdateProductV2ParamsSchema, } from "@api/models.js"; import { z } from "zod/v4"; + import { ApiProductSchema } from "./previousVersions/apiProduct.js"; // Note: The meta with id is added in openapi.ts to avoid duplicate registration @@ -112,7 +112,9 @@ export const productOps = { description: "Product deleted successfully", content: { "application/json": { - schema: SuccessResponseSchema, + schema: z.object({ + success: z.boolean(), + }), }, }, }, diff --git a/shared/api/versionUtils/ApiVersionClass.ts b/shared/api/versionUtils/ApiVersionClass.ts index dda6232f8..4cc15b700 100644 --- a/shared/api/versionUtils/ApiVersionClass.ts +++ b/shared/api/versionUtils/ApiVersionClass.ts @@ -1,6 +1,9 @@ -import { ApiVersion, API_VERSIONS } from "./ApiVersion.js"; +import { API_VERSIONS, type ApiVersion } from "./ApiVersion.js"; import type { VersionMetadata } from "./versionRegistry.js"; -import { getVersionMetadata, getVersionsSorted } from "./versionRegistryUtils.js"; +import { + getVersionMetadata, + getVersionsSorted, +} from "./versionRegistryUtils.js"; /** * ApiVersionClass - Encapsulates version comparison logic diff --git a/shared/api/versionUtils/versionRegistryUtils.ts b/shared/api/versionUtils/versionRegistryUtils.ts index 2ac64d209..42f63613a 100644 --- a/shared/api/versionUtils/versionRegistryUtils.ts +++ b/shared/api/versionUtils/versionRegistryUtils.ts @@ -1,4 +1,4 @@ -import { API_VERSIONS, ApiVersion } from "./ApiVersion.js"; +import { API_VERSIONS, type ApiVersion } from "./ApiVersion.js"; import { VERSION_REGISTRY, type VersionMetadata } from "./versionRegistry.js"; /** @@ -24,7 +24,9 @@ export function getVersionMetadata({ return VERSION_REGISTRY[version]; } -export function isValidVersion(params: { version: string }): params is { version: ApiVersion } { +export function isValidVersion(params: { + version: string; +}): params is { version: ApiVersion } { return API_VERSIONS.includes(params.version as ApiVersion); } diff --git a/shared/index.ts b/shared/index.ts index 66a131622..03280c50c 100644 --- a/shared/index.ts +++ b/shared/index.ts @@ -2,10 +2,10 @@ import * as schemas from "./db/schema.js"; export { schemas }; +export * from "./api/apiUtils.js"; // API MODELS export * from "./api/models.js"; export * from "./api/operations.js"; -export * from "./api/platform/platformModels.js"; // API VERSIONING SYSTEM export * from "./api/versionUtils/versionUtils.js"; @@ -142,6 +142,10 @@ export * from "./models/subModels/subTable.js"; export * from "./utils/displayUtils.js"; export * from "./utils/index.js"; export * from "./utils/intervalUtils.js"; +export * from "./utils/planFeatureUtils/itemsToPlanFeatures.js"; +export * from "./utils/planFeatureUtils/planFeatureIntervals.js"; +export * from "./utils/planFeatureUtils/planFeaturesToItems.js"; +export * from "./utils/planFeatureUtils/planToItems.js"; export * from "./utils/productDisplayUtils/sortProductItems.js"; export * from "./utils/productDisplayUtils.js"; export * from "./utils/productUtils/priceToInvoiceAmount.js"; @@ -151,10 +155,6 @@ export * from "./utils/productV2Utils/productItemUtils/getProductItemRes.js"; export * from "./utils/productV2Utils/productItemUtils/itemIntervalUtils.js"; export * from "./utils/productV3Utils/mapToProductV3.js"; export * from "./utils/productV3Utils/productItemUtils/productV3ItemUtils.js"; -export * from "./utils/planFeatureUtils/itemsToPlanFeatures.js"; -export * from "./utils/planFeatureUtils/planFeaturesToItems.js"; -export * from "./utils/planFeatureUtils/planFeatureIntervals.js"; -export * from "./utils/planFeatureUtils/planToItems.js"; export * from "./utils/rewardUtils/rewardMigrationUtils.js"; export enum ResetInterval { diff --git a/shared/models/checkModels/checkPreviewModels.ts b/shared/models/checkModels/checkPreviewModels.ts index 73840c378..ca287902a 100644 --- a/shared/models/checkModels/checkPreviewModels.ts +++ b/shared/models/checkModels/checkPreviewModels.ts @@ -11,6 +11,7 @@ export enum AttachScenario { Downgrade = "downgrade", Cancel = "cancel", Expired = "expired", + PastDue = "past_due", } export interface PreviewItem { diff --git a/shared/models/orgModels/frontendOrg.ts b/shared/models/orgModels/frontendOrg.ts index fda39c01d..82fb0ff27 100644 --- a/shared/models/orgModels/frontendOrg.ts +++ b/shared/models/orgModels/frontendOrg.ts @@ -8,10 +8,20 @@ export const FrontendOrgSchema = z.object({ success_url: z.string(), default_currency: z.string(), - stripe_connected: z.boolean(), created_at: z.number(), test_pkey: z.string().nullable(), live_pkey: z.string().nullable(), + + stripe_connection: z.string(), + master: z + .object({ + id: z.string(), + name: z.string(), + slug: z.string(), + }) + .nullable(), + through_master: z.boolean(), + onboarded: z.boolean(), }); export type FrontendOrg = z.infer; diff --git a/shared/models/orgModels/orgRelations.ts b/shared/models/orgModels/orgRelations.ts index 9955da110..a2dbac475 100644 --- a/shared/models/orgModels/orgRelations.ts +++ b/shared/models/orgModels/orgRelations.ts @@ -4,8 +4,15 @@ import { apiKeys } from "../devModels/apiKeyTable.js"; import { features } from "../featureModels/featureTable.js"; import { organizations } from "./orgTable.js"; -export const organizationsRelations = relations(organizations, ({ many }) => ({ - api_keys: many(apiKeys), - features: many(features), - members: many(member), -})); +export const organizationsRelations = relations( + organizations, + ({ many, one }) => ({ + api_keys: many(apiKeys), + features: many(features), + members: many(member), + master: one(organizations, { + fields: [organizations.created_by], + references: [organizations.id], + }), + }), +); diff --git a/shared/models/orgModels/orgTable.ts b/shared/models/orgModels/orgTable.ts index 3f723d03a..1df67889f 100644 --- a/shared/models/orgModels/orgTable.ts +++ b/shared/models/orgModels/orgTable.ts @@ -22,16 +22,15 @@ export type StripeConfig = { live_webhook_secret?: string; sandbox_success_url?: string; success_url?: string; + + test_connect_webhook_secret?: string; + live_connect_webhook_secret?: string; }; export type OrgProcessorConfig = { success_url: string; }; -// logo: text("logo"), -// createdAt: timestamp("created_at").notNull(), -// metadata: text("metadata"), - export interface VersionConfig { sandbox?: string; live?: string; @@ -39,6 +38,12 @@ export interface VersionConfig { // live_webhooks: string; } +export type StripeConnectConfig = { + default_account_id?: string; + account_id?: string; + master_org_id?: string; +}; + export const organizations = pgTable( "organizations", { @@ -53,18 +58,34 @@ export const organizations = pgTable( // Stripe default_currency: text("default_currency").default("usd"), - stripe_connected: boolean("stripe_connected").default(false), + stripe_connected: boolean("stripe_connected").default(false), stripe_config: jsonb("stripe_config").$type(), + + test_stripe_connect: jsonb("test_stripe_connect") + .$type() + .default({} as StripeConnectConfig), + + live_stripe_connect: jsonb("live_stripe_connect") + .$type() + .default({} as StripeConnectConfig), + + // stripe_connect: jsonb("stripe_connect") + // .$type() + // .default({} as StripeConnectConfig) + // .notNull(), + test_pkey: text("test_pkey"), live_pkey: text("live_pkey"), + svix_config: jsonb("svix_config") .$type() .default(sql`'{}'::jsonb`), + created_at: numeric({ mode: "number" }), config: jsonb().default({}).notNull().$type(), created_by: text("created_by"), - // version: jsonb("version").$type().default(sql`'{}'::jsonb`), + onboarded: boolean("onboarded").default(false), }, (table) => [ unique("organizations_test_pkey_key").on(table.test_pkey), @@ -72,4 +93,13 @@ export const organizations = pgTable( ], ); -export type Organization = typeof organizations.$inferSelect; +export type Organization = typeof organizations.$inferSelect & { + master: Organization | null; +}; + +// Multi tenancy flow <-> stripe connect... +// Create org in Autumn, don't need stripe connect key, we create an Autumn connect account for them. +// Connect own stripe to sandbox / prod +// 1. OAuth to link their stripe account (?) -> need to use access token though +// 2. Paste in their secret key +// 3. Onboard onto Stripe connect (?) diff --git a/shared/models/productV2Models/productItemModels/productItemModels.ts b/shared/models/productV2Models/productItemModels/productItemModels.ts index 38c76a944..f00a7fea5 100644 --- a/shared/models/productV2Models/productItemModels/productItemModels.ts +++ b/shared/models/productV2Models/productItemModels/productItemModels.ts @@ -90,8 +90,13 @@ export const ProductItemSchema = z.object({ // Others // carry_over_usage: z.boolean().nullish(), reset_usage_when_enabled: z.boolean().nullish(), - config: ProductItemConfigSchema.nullish(), + display: z + .object({ + primary_text: z.string(), + secondary_text: z.string().nullish(), + }) + .nullish(), // Stored in backend created_at: z.number().nullish(), diff --git a/shared/models/productV3Models/productV3Response.ts b/shared/models/productV3Models/productV3Response.ts index 4738819c6..ca45c9420 100644 --- a/shared/models/productV3Models/productV3Response.ts +++ b/shared/models/productV3Models/productV3Response.ts @@ -1,3 +1,4 @@ +import { ApiProductPropertiesSchema } from "@api/products/previousVersions/apiProduct.js"; import { z } from "zod/v4"; import { AttachScenario } from "../checkModels/checkPreviewModels.js"; import { AppEnv } from "../genModels/genEnums.js"; @@ -29,4 +30,5 @@ export const PlanResponseSchema = z.object({ // base_variant_id: z.string().nullable(), scenario: z.nativeEnum(AttachScenario).optional(), + properties: ApiProductPropertiesSchema.optional(), }); diff --git a/shared/package.json b/shared/package.json index 840a86285..7f13a0be7 100644 --- a/shared/package.json +++ b/shared/package.json @@ -7,7 +7,8 @@ "exports": { ".": { "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "import": "./dist/index.js", + "default": "./dist/index.js" } }, "author": "Recase Inc.", diff --git a/shared/utils/utils.ts b/shared/utils/utils.ts index 1a97024b1..ece36c37f 100644 --- a/shared/utils/utils.ts +++ b/shared/utils/utils.ts @@ -13,7 +13,13 @@ export const sumValues = (vals: number[]) => { return vals.reduce((acc, curr) => acc + curr, 0); }; -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()); diff --git a/vite/package.json b/vite/package.json index 7715864ec..a5f761c27 100644 --- a/vite/package.json +++ b/vite/package.json @@ -16,8 +16,10 @@ "author": "Recase Inc.", "license": "Apache-2.0", "dependencies": { + "@amplitude/unified": "^1.0.0-beta.9", "@autumn/shared": "workspace:*", "@better-auth/stripe": "^1.2.12", + "@date-fns/utc": "^2.1.1", "@fortawesome/free-brands-svg-icons": "^6.7.2", "@fortawesome/react-fontawesome": "^0.2.2", "@heroicons/react": "^2.2.0", diff --git a/vite/src/App.tsx b/vite/src/App.tsx index d5d01a619..e3345f40e 100644 --- a/vite/src/App.tsx +++ b/vite/src/App.tsx @@ -1,9 +1,12 @@ import { AppEnv } from "@autumn/shared"; import { init } from "@squircle/core"; import * as React from "react"; +import { useEffect } from "react"; import { BrowserRouter, Route, Routes } from "react-router"; import { MainLayout } from "./app/layout"; import { OnboardingLayout } from "./app/OnboardingLayout"; +import { useSession } from "./lib/auth-client"; +import { identifyUser } from "./utils/posthogTracking"; import { AdminView } from "./views/admin/AdminView"; import { AcceptInvitation } from "./views/auth/AcceptInvitation"; import { PasswordSignIn } from "./views/auth/components/PasswordSignIn"; @@ -15,7 +18,6 @@ import CustomerView from "./views/customers/customer/CustomerView"; import CustomerProductView from "./views/customers/customer/product/CustomerProductView"; import { DefaultView } from "./views/DefaultView"; import DevScreen from "./views/developer/DevView"; -import OnboardingView2 from "./views/onboarding2/OnboardingView2"; import OnboardingView3 from "./views/onboarding3/OnboardingView3"; import ProductsView from "./views/products/ProductsView"; import PlanEditorView from "./views/products/plan/PlanEditorView"; @@ -27,6 +29,16 @@ export function SquircleProvider({ children }: { children: React.ReactNode }) { } export default function App() { + const { data } = useSession(); + + useEffect(() => { + if (data) { + identifyUser({ + email: data.user.email, + name: data.user.name, + }); + } + }, [data]); return ( @@ -36,15 +48,13 @@ export default function App() { {/* Onboarding routes without sidebar */} }> - } /> + } /> }> } /> } /> } /> - } /> - } /> { @@ -47,20 +45,14 @@ export function MainLayout() { }, [handleApiError]); useEffect(() => { - // Identify user - if (data && process.env.NODE_ENV !== "development") { - const email = data.user.email; - - posthog?.identify(email, { - email, - name: data.user.name, - id: data.user.id, - }); + // Only redirect if org is loaded and user is not onboarded + if (!orgLoading && org && !org.onboarded) { + navigate("/sandbox/onboarding"); } - }, [data, posthog]); + }, [org, orgLoading, navigate]); // 1. If not loaded, show loading screen - if (isPending) { + if (isPending || orgLoading) { return ( { useDevQuery(); useAutumnFlags(); - useProductsQuery(); useFeaturesQuery(); useRewardsQuery(); useCusSearchQuery(); @@ -152,7 +143,7 @@ const MainContent = () => { variant="default" className="h-6 border border-t8 bg-transparent text-t8 hover:bg-t8 hover:text-white font-mono rounded-xs ml-auto absolute right-4" onClick={() => { - navigateTo("/onboarding3", navigate, AppEnv.Sandbox); + navigateTo("/onboarding", navigate, AppEnv.Sandbox); }} > Onboarding diff --git a/vite/src/components/autumn/PlanCardPreview.tsx b/vite/src/components/autumn/PlanCardPreview.tsx index 6de19534d..15f077ae3 100644 --- a/vite/src/components/autumn/PlanCardPreview.tsx +++ b/vite/src/components/autumn/PlanCardPreview.tsx @@ -1,24 +1,20 @@ import { + isFeaturePriceItem, mapToProductV3, type ProductItem, + type ProductV2, productV2ToFeatureItems, } from "@autumn/shared"; -import type { Product } from "autumn-js"; +import { useState } from "react"; import { Button } from "@/components/v2/buttons/Button"; import { Card, CardContent, CardHeader } from "@/components/v2/cards/Card"; import { Separator } from "@/components/v2/separator"; import { cn } from "@/lib/utils"; import { keyToTitle } from "@/utils/formatUtils/formatTextUtils"; -import { CodeSpan } from "@/views/onboarding2/integrate/components/CodeSpan"; import { PlanFeatureIcon } from "@/views/products/plan/components/plan-card/PlanFeatureIcon"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "../v2/tooltips/Tooltip"; interface PlanCardPreviewProps { - product: Product; + product: ProductV2; buttonText?: string; onButtonClick?: () => void; recommended?: boolean; @@ -36,7 +32,7 @@ const PlanFeatureRowPreview = ({ item }: { item: ProductItem }) => { const display = item.display || { primary_text: "", secondary_text: "" }; return ( -
+
{/* Left side - Icons and text */}
@@ -68,13 +64,27 @@ export const PlanCardPreview = ({ recommended = false, disabled = false, }: PlanCardPreviewProps) => { - const productV3 = mapToProductV3({ product }); - const featureItems = productV2ToFeatureItems({ items: product.items }); + const productV3 = mapToProductV3({ product: product as ProductV2 }); + const featureItems = productV2ToFeatureItems({ + items: product.items as ProductItem[], + }); + + const [buttonLoading, setButtonLoading] = useState(false); + const handleButtonClick = async () => { + setButtonLoading(true); + try { + await onButtonClick?.(); + } catch (error) { + console.error(error); + } finally { + setButtonLoading(false); + } + }; return ( @@ -84,7 +94,12 @@ export const PlanCardPreview = ({

{product.name}

{/* Price */} - {productV3.price?.amount ? ( + {product.items.filter((x) => isFeaturePriceItem(x as ProductItem)) + .length > 0 && + (productV3.price?.amount === 0 || productV3.price == null) ? ( + Varies + ) : typeof productV3.price?.amount === "number" && + productV3.price.amount > 0 ? ( ${productV3.price.amount}/ {keyToTitle(productV3.price.interval ?? "once", { @@ -106,7 +121,7 @@ export const PlanCardPreview = ({ - + {/* Feature list */} {featureItems.length > 0 && (
@@ -120,89 +135,93 @@ export const PlanCardPreview = ({ )} {/* Action button */} - - - - + {/* */} + {/* */} + + {/* When checking out in test mode -
use 4242 4242 4242 4242 as the card number,
04/42 as the expiry date, and{" "} any CVC. -
-
+ */} + {/*
*/} ); diff --git a/vite/src/components/autumn/pricing-table-preview.tsx b/vite/src/components/autumn/pricing-table-preview.tsx index a2c9f2c17..3b7acd421 100644 --- a/vite/src/components/autumn/pricing-table-preview.tsx +++ b/vite/src/components/autumn/pricing-table-preview.tsx @@ -1,12 +1,13 @@ +import type { ProductV2 } from "@autumn/shared"; +import { InfoIcon } from "@phosphor-icons/react"; import type { Product } from "autumn-js"; import { useCustomer } from "autumn-js/react"; - import { useOrg } from "@/hooks/common/useOrg"; import OnboardingCheckoutDialog from "@/views/onboarding3/OnboardingCheckoutDialog"; import { PlanCardPreview } from "./PlanCardPreview"; interface PricingTableProps { - products?: Product[]; + products?: ProductV2[]; setConnectStripeOpen: (open: boolean) => void; onCheckoutComplete?: () => void; } @@ -26,12 +27,7 @@ export default function PricingTablePreview({ return null; } - const handleSubscribe = async (product: Product) => { - if (!org?.stripe_connected) { - setConnectStripeOpen(true); - return; - } - + const handleSubscribe = async (product: ProductV2) => { if (product.id) { try { await checkout({ @@ -77,20 +73,60 @@ export default function PricingTablePreview({ } else if (productCount === 2) { return "flex flex-col gap-6 max-w-2xl mx-auto px-4 sm:grid sm:grid-cols-2 sm:flex-none"; // Vertical on mobile, 2 columns on sm+ } else { - return "flex flex-col gap-6 max-w-7xl mx-auto px-4 sm:grid md:grid-cols-2 2xl:grid-cols-3 sm:flex-none"; // Vertical on mobile, 2 columns on sm+, 3 on lg+ + return "flex flex-col gap-6 max-w-7xl mx-auto px-4 sm:grid md:grid-cols-2 xl:grid-cols-3 sm:flex-none"; // Vertical on mobile, 2 columns on sm+, 3 on lg+ } }; return ( -
-
+
+
+
+
+ +
+ Test mode checkout: +
+
+
+
+ Card number +
+
+
+ 4242 4242 4242 4242 +
+
+
+
+
+ Expiry date +
+
+
+ Any +
+
+
+
+
+ CVC +
+
+
+ Any +
+
+
+
+
+
{products.map((product, index) => ( handleSubscribe(product)} - recommended={isRecommended(product)} + recommended={isRecommended(product as Product)} disabled={ (product.scenario === "active" && !product.properties?.updateable) || diff --git a/vite/src/components/autumn/pricing-table.tsx b/vite/src/components/autumn/pricing-table.tsx index 84522ef67..47691ecec 100644 --- a/vite/src/components/autumn/pricing-table.tsx +++ b/vite/src/components/autumn/pricing-table.tsx @@ -1,16 +1,13 @@ -import React from "react"; -import { Loader2 } from "lucide-react"; - -import { createContext, useContext, useState } from "react"; -import { cn } from "@/lib/utils"; -import { Switch } from "@/components/ui/switch"; -import { Button } from "@/components/ui/button"; -import CheckoutDialog from "@/components/autumn/checkout-dialog"; -import { getPricingTableContent } from "@/lib/autumn/pricing-table-content"; import type { Product, ProductItem } from "autumn-js"; - import { useCustomer } from "autumn-js/react"; +import { Loader2 } from "lucide-react"; +import React, { createContext, useContext, useState } from "react"; +import CheckoutDialog from "@/components/autumn/checkout-dialog"; +import { Button } from "@/components/ui/button"; +import { Switch } from "@/components/ui/switch"; import { useOrg } from "@/hooks/common/useOrg"; +import { getPricingTableContent } from "@/lib/autumn/pricing-table-content"; +import { cn } from "@/lib/utils"; export default function PricingTable({ products, @@ -68,11 +65,6 @@ export default function PricingTable({ product.scenario === "scheduled", onClick: async () => { - if (!org.stripe_connected) { - setConnectStripeOpen(true); - return; - } - if (product.id) { const result = await checkout({ productId: product.id, diff --git a/vite/src/components/general/modal-components/InfoTooltip.tsx b/vite/src/components/general/modal-components/InfoTooltip.tsx index 4b91f97ba..8860e65a6 100644 --- a/vite/src/components/general/modal-components/InfoTooltip.tsx +++ b/vite/src/components/general/modal-components/InfoTooltip.tsx @@ -16,8 +16,15 @@ export const InfoTooltip = ({ } & TooltipContentProps) => { return ( - - + e.preventDefault()} + > + {children} diff --git a/vite/src/components/ui/button.tsx b/vite/src/components/ui/button.tsx index 8885173b8..0c619bb53 100644 --- a/vite/src/components/ui/button.tsx +++ b/vite/src/components/ui/button.tsx @@ -126,12 +126,12 @@ const Button = React.forwardRef( > {isLoading && } {startIcon && !isLoading && <>{startIcon}} - {!isLoading && !startIcon && variant == "add" && !disableStartIcon && ( + {!isLoading && !startIcon && variant === "add" && !disableStartIcon && ( )} {!isLoading && !startIcon && - variant == "analyse" && + variant === "analyse" && !disableStartIcon && } {children} {endIcon && !isLoading && <>{endIcon}} diff --git a/vite/src/components/v2/badges/PlanTypeBadge.tsx b/vite/src/components/v2/badges/PlanTypeBadge.tsx index 3076dca03..4dda1ace9 100644 --- a/vite/src/components/v2/badges/PlanTypeBadge.tsx +++ b/vite/src/components/v2/badges/PlanTypeBadge.tsx @@ -1,6 +1,11 @@ import { PlusSquareIcon } from "@phosphor-icons/react"; import { cva, type VariantProps } from "class-variance-authority"; import { DefaultIcon, FreeTrialIcon } from "@/components/v2/icons/AutumnIcons"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/v2/tooltips/Tooltip"; import { cn } from "@/lib/utils"; const badgeVariants = cva( @@ -21,19 +26,24 @@ const badgeVariants = cva( export interface PlanTypeBadgeProps extends VariantProps { className?: string; + iconOnly?: boolean; } -export const PlanTypeBadge = ({ variant, className }: PlanTypeBadgeProps) => { +export const PlanTypeBadge = ({ + variant, + className, + iconOnly, +}: PlanTypeBadgeProps) => { const getIcon = () => { switch (variant) { case "default": - return ; + return ; case "freeTrial": - return ; + return ; case "addon": return ; default: - return ; + return ; } }; @@ -50,10 +60,31 @@ export const PlanTypeBadge = ({ variant, className }: PlanTypeBadgeProps) => { } }; + const getTooltipContent = () => { + switch (variant) { + case "default": + return "This plan will enable by default for all new users."; + case "freeTrial": + return "This plan has a free trial period."; + case "addon": + return "This plan is an add-on that can be bought together with your base plans (eg, for top ups)."; + } + }; + return ( -
- {getIcon()} - {getLabel()} -
+ + +
+ {getIcon()} + {!iconOnly && {getLabel()}} +
+
+ + {getTooltipContent() !== null && ( + {getTooltipContent()} + )} +
); }; diff --git a/vite/src/components/v2/badges/PlanTypeBadges.tsx b/vite/src/components/v2/badges/PlanTypeBadges.tsx index f771b95f5..f55128992 100644 --- a/vite/src/components/v2/badges/PlanTypeBadges.tsx +++ b/vite/src/components/v2/badges/PlanTypeBadges.tsx @@ -4,14 +4,24 @@ import { PlanTypeBadge } from "./PlanTypeBadge"; interface PlanTypeBadgesProps { product: ProductV2; className?: string; + iconOnly?: boolean; } -export const PlanTypeBadges = ({ product, className }: PlanTypeBadgesProps) => { +export const PlanTypeBadges = ({ + product, + className, + iconOnly = false, +}: PlanTypeBadgesProps) => { const badges = []; if (product.is_default) { badges.push( - , + , ); } @@ -21,13 +31,19 @@ export const PlanTypeBadges = ({ product, className }: PlanTypeBadgesProps) => { key="freeTrial" variant="freeTrial" className={className} + iconOnly={iconOnly} />, ); } if (product.is_add_on) { badges.push( - , + , ); } diff --git a/vite/src/components/v2/buttons/Button.tsx b/vite/src/components/v2/buttons/Button.tsx index d93f4d146..adf800b44 100644 --- a/vite/src/components/v2/buttons/Button.tsx +++ b/vite/src/components/v2/buttons/Button.tsx @@ -44,6 +44,12 @@ const buttonVariants = cva( focus-visible:border-destructive-border active:border-destructive-border `, + + dotted: `bg-white border border-dashed border-neutral-300 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.02)] + hover:border-primary hover:border-solid + focus-visible:border-primary focus-visible:border-solid + active:border-primary active:border-solid + `, }, size: { default: "py-1 !px-[7px] text-body h-input", @@ -134,6 +140,9 @@ const Button = React.forwardRef( case "destructive": return "active:!bg-destructive active:!border-transparent focus-visible:!bg-destructive focus-visible:!border-transparent"; + case "dotted": + return "active:!bg-white active:!border-dashed active:!border-neutral-300 focus-visible:!bg-white focus-visible:!border-dashed focus-visible:!border-neutral-300"; + default: return ""; } diff --git a/vite/src/components/v2/buttons/GroupedTabButton.tsx b/vite/src/components/v2/buttons/GroupedTabButton.tsx index b8cbf1bf2..0f0bbe912 100644 --- a/vite/src/components/v2/buttons/GroupedTabButton.tsx +++ b/vite/src/components/v2/buttons/GroupedTabButton.tsx @@ -39,9 +39,9 @@ export const GroupedTabButton = ({ "bg-light-purple text-primary shadow-[0px_3px_4px_0px_inset_rgba(0,0,0,0.04)]", !isActive && "bg-white shadow-[0px_-3px_4px_0px_inset_rgba(0,0,0,0.04)]", - isFirst && "rounded-l-md border-l", + isFirst && "rounded-l-lg border-l", !isFirst && "border-l-0", - isLast && "rounded-r-md", + isLast && "rounded-r-lg", )} > {isTwoTab && isFirst && option.icon && ( diff --git a/vite/src/components/v2/icons/AutumnIcons.tsx b/vite/src/components/v2/icons/AutumnIcons.tsx index 3390939e1..7a72b68ea 100644 --- a/vite/src/components/v2/icons/AutumnIcons.tsx +++ b/vite/src/components/v2/icons/AutumnIcons.tsx @@ -1,3 +1,4 @@ +/** biome-ignore-all lint/a11y/noSvgWithoutTitle: needed */ import { useId } from "react"; export const FeatureArrowIcon = () => { @@ -279,9 +280,11 @@ export const ContinuousUseIcon = () => { export const DefaultIcon = ({ size = 16, color = "#666666", + hideTitle = false, }: { size?: number; color?: string; + hideTitle?: boolean; }) => { return ( - Default + {!hideTitle && Default} { return ( - Free Trial + {!hideTitle && Free Trial} void; + className?: string; + disabled?: boolean; +}) => { + const [open, setOpen] = useState(false); + + return ( + + + + + + + + No currency found. + + + {stripeCurrencyCodes.map((currency) => ( + { + setDefaultCurrency(value.toUpperCase()); + setOpen(false); + }} + className="p-2 flex items-center justify-between" + > + {currency.currency} - {currency.code} + + + ))} + + + + + + ); +}; diff --git a/vite/src/hooks/common/useAutumnFlags.tsx b/vite/src/hooks/common/useAutumnFlags.tsx index 1d72a3ad4..6287360cd 100644 --- a/vite/src/hooks/common/useAutumnFlags.tsx +++ b/vite/src/hooks/common/useAutumnFlags.tsx @@ -1,7 +1,7 @@ -import { useEffect } from "react"; -import { notNullish } from "@/utils/genUtils"; import { useCustomer } from "autumn-js/react"; +import { useEffect } from "react"; import { useLocalStorage } from "@/hooks/common/useLocalStorage"; +import { notNullish } from "@/utils/genUtils"; export const useAutumnFlags = () => { const { customer } = useCustomer(); @@ -9,6 +9,8 @@ export const useAutumnFlags = () => { const [flags, setFlags] = useLocalStorage("autumn.flags", { pkey: false, webhooks: false, + stripe_key: false, + platform: false, }); useEffect(() => { @@ -17,16 +19,20 @@ export const useAutumnFlags = () => { const nextFlags = { pkey: notNullish(customer.features.pkey), webhooks: notNullish(customer.features.webhooks), + stripe_key: notNullish(customer.features.stripe_key), + platform: notNullish(customer.features.platform), }; // Only update storage/state when values actually change if ( flags.pkey !== nextFlags.pkey || - flags.webhooks !== nextFlags.webhooks + flags.webhooks !== nextFlags.webhooks || + flags.stripe_key !== nextFlags.stripe_key || + flags.platform !== nextFlags.platform ) { setFlags(nextFlags); } - }, [customer?.features?.pkey, customer?.features?.webhooks]); + }, [customer?.features]); return flags; }; diff --git a/vite/src/hooks/common/useOrg.tsx b/vite/src/hooks/common/useOrg.tsx index 4fc8176dc..19264478f 100644 --- a/vite/src/hooks/common/useOrg.tsx +++ b/vite/src/hooks/common/useOrg.tsx @@ -12,7 +12,7 @@ export const useOrg = () => { try { const { data } = await axiosInstance.get("/organization"); return data; - } catch (error) { + } catch { return null; } }; @@ -27,25 +27,25 @@ export const useOrg = () => { queryFn: fetcher, }); - 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(() => { + 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(); + } + }; + // 1. If no org... if (!org && !isLoading) { handleNoActiveOrg(); } - }, [org, orgList]); + }, [org, orgList, isLoading]); return { org: org as FrontendOrg, isLoading, error, mutate: refetch }; }; diff --git a/vite/src/hooks/queries/useOrgStripeQuery.tsx b/vite/src/hooks/queries/useOrgStripeQuery.tsx index a0ebea4d4..a71cea7d3 100644 --- a/vite/src/hooks/queries/useOrgStripeQuery.tsx +++ b/vite/src/hooks/queries/useOrgStripeQuery.tsx @@ -11,7 +11,7 @@ export const useOrgStripeQuery = () => { const fetchStripeAccount = async () => { const { data } = await axiosInstance.get( - "/organization/stripe", + "/v1/organization/stripe", ); return data; }; diff --git a/vite/src/index.css b/vite/src/index.css index b4ab18db8..09290534e 100644 --- a/vite/src/index.css +++ b/vite/src/index.css @@ -357,11 +357,17 @@ input[type="number"]::-webkit-inner-spin-button { ); animation: shimmer 1.5s infinite; } + @keyframes shimmer { 100% { left: 150%; } } +@-webkit-keyframes shimmer { + 100% { + left: 150%; + } +} .shimmer-hover { position: relative; @@ -385,10 +391,12 @@ input[type="number"]::-webkit-inner-spin-button { opacity: 0; transition: opacity 0.2s; } + .shimmer-hover:hover::after { animation: shimmer-once 0.8s; opacity: 1; } + @keyframes shimmer-once { 0% { left: -150%; diff --git a/vite/src/main.tsx b/vite/src/main.tsx index dcfbcc269..91834b7fd 100644 --- a/vite/src/main.tsx +++ b/vite/src/main.tsx @@ -14,25 +14,29 @@ import { createRoot } from "react-dom/client"; import App from "./App"; const queryClient = new QueryClient({ - defaultOptions: { - // queries: { - // refetchInterval: 0, - // }, - }, + defaultOptions: {}, }); +const shouldInitializePostHog = process.env.NODE_ENV === "production"; + createRoot(document.getElementById("root")!).render( - {process.env.NODE_ENV === "development" ? ( - - ) : ( + {/* */} + {shouldInitializePostHog ? ( + ) : ( + )} {/* */} diff --git a/vite/src/services/OrgService.tsx b/vite/src/services/OrgService.tsx index b14fb5543..fc00b9274 100644 --- a/vite/src/services/OrgService.tsx +++ b/vite/src/services/OrgService.tsx @@ -1,4 +1,4 @@ -import { AxiosInstance } from "axios"; +import type { AxiosInstance } from "axios"; export class OrgService { static async get(axiosInstance: AxiosInstance) { @@ -10,10 +10,10 @@ export class OrgService { } static async connectStripe(axiosInstance: AxiosInstance, data: any) { - return await axiosInstance.post(`/organization/stripe`, data); + return await axiosInstance.post(`/v1/organization/stripe`, data); } static async disconnectStripe(axiosInstance: AxiosInstance) { - return await axiosInstance.delete(`/organization/stripe`); + return await axiosInstance.delete(`/v1/organization/stripe`); } } diff --git a/vite/src/utils/genUtils.ts b/vite/src/utils/genUtils.ts index f448315da..8dcf4a70e 100644 --- a/vite/src/utils/genUtils.ts +++ b/vite/src/utils/genUtils.ts @@ -157,3 +157,12 @@ export const getMetaKey = () => { } return "Ctrl"; }; +/** + * Throws an error with backend message if available, otherwise rethrows original error + */ +export const throwBackendError = (error: any): never => { + if (error?.response?.data?.message) { + throw new Error(error.response.data.message); + } + throw error; +}; diff --git a/vite/src/utils/linkUtils.ts b/vite/src/utils/linkUtils.ts index edd2068f2..d53996431 100644 --- a/vite/src/utils/linkUtils.ts +++ b/vite/src/utils/linkUtils.ts @@ -61,3 +61,16 @@ export const getStripeInvoiceLink = ({ const withTest = env === AppEnv.Live ? "" : "/test"; return `${baseUrl}${accountPath}${withTest}/invoices/${stripeInvoice.id || stripeInvoice.stripe_id}`; }; + +export const getStripeDashboardLink = ({ + env, + accountId, +}: { + env: AppEnv; + accountId?: string; +}) => { + const baseUrl = `https://dashboard.stripe.com`; + const accountPath = accountId ? `/${accountId}` : ""; + const withTest = env === AppEnv.Live ? "" : "/test"; + return `${baseUrl}${accountPath}${withTest}/dashboard`; +}; diff --git a/vite/src/utils/posthogTracking.ts b/vite/src/utils/posthogTracking.ts new file mode 100644 index 000000000..1b77ee986 --- /dev/null +++ b/vite/src/utils/posthogTracking.ts @@ -0,0 +1,107 @@ +import posthog from "posthog-js"; + +// Toggle this to enable/disable tracking in development +export const TRACK_IN_DEVELOPMENT = false; + +/** + * Wrapper function to conditionally track events based on environment + */ +function trackEvent(eventName: string, properties?: Record) { + try { + // Skip tracking in development unless explicitly enabled + if (process.env.NODE_ENV === "development" && !TRACK_IN_DEVELOPMENT) { + console.log(`[DEV] Would track event: ${eventName}`, properties); + return; + } + + posthog.capture(eventName, properties); + } catch (error) { + console.error(`Error tracking event: ${eventName}`, error); + } +} + +/** + * Wrapper function to conditionally identify users based on environment + */ +function identifyUserInPostHog( + distinctId: string, + properties?: Record, +) { + // Skip tracking in development unless explicitly enabled + if (process.env.NODE_ENV === "development" && !TRACK_IN_DEVELOPMENT) { + console.log(`[DEV] Would identify user: ${distinctId}`, properties); + return; + } + + posthog.identify(distinctId, properties); +} + +/** + * Identify user in PostHog (call when user data is available) + */ +export function identifyUser({ + email, + name, +}: { + email: string; + name?: string; +}) { + identifyUserInPostHog(email, { + email, + ...(name && { name }), + }); +} + +/** + * Track user sign-up + */ +export function trackSignUp() { + trackEvent("user_signed_up"); +} + +/** + * Track onboarding product creation + */ +export function trackOnboardingProductCreation({ + productType, +}: { + productType: "free" | "paid"; +}) { + trackEvent("onboarding_product_created", { + product_type: productType, + }); +} + +/** + * Track onboarding feature creation + */ +export function trackOnboardingFeatureCreation({ + featureType, +}: { + featureType: string; +}) { + trackEvent("onboarding_feature_created", { + feature_type: featureType, + }); +} + +/** + * Track onboarding feature configuration + */ +export function trackOnboardingFeatureConfigured() { + trackEvent("onboarding_feature_configured"); +} + +/** + * Track onboarding playground completion + */ +export function trackOnboardingPlaygroundCompleted() { + trackEvent("onboarding_playground_completed"); +} + +/** + * Track onboarding integration completion + */ +export function trackOnboardingIntegrationCompleted() { + trackEvent("onboarding_integration_completed"); +} diff --git a/vite/src/views/DefaultView.tsx b/vite/src/views/DefaultView.tsx index 360aeac80..931f0df01 100644 --- a/vite/src/views/DefaultView.tsx +++ b/vite/src/views/DefaultView.tsx @@ -4,7 +4,7 @@ import ErrorScreen from "./general/ErrorScreen"; export const DefaultView = () => { const { pathname } = useLocation(); - if (pathname == "/") { + if (pathname === "/") { return ; } diff --git a/vite/src/views/auth/SignIn.tsx b/vite/src/views/auth/SignIn.tsx index e3c19b350..3f68ef30b 100644 --- a/vite/src/views/auth/SignIn.tsx +++ b/vite/src/views/auth/SignIn.tsx @@ -2,12 +2,14 @@ import { faGoogle } from "@fortawesome/free-brands-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Mail } from "lucide-react"; import { useEffect, useState } from "react"; -import { useSearchParams } from "react-router"; +import { useNavigate } from "react-router"; import { toast } from "sonner"; import { CustomToaster } from "@/components/general/CustomToaster"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { authClient, signIn, useSession } from "@/lib/auth-client"; +import { useOrg } from "@/hooks/common/useOrg"; +import { authClient, signIn } from "@/lib/auth-client"; +import { cn } from "@/lib/utils"; import { getBackendErr } from "@/utils/genUtils"; import { OTPSignIn } from "./components/OTPSignIn"; @@ -16,30 +18,27 @@ export const SignIn = () => { const [googleLoading, setGoogleLoading] = useState(false); const [sendOtpLoading, setSendOtpLoading] = useState(false); const [otpSent, setOtpSent] = useState(false); - const { data: session } = useSession(); + // const { data: session } = useSession(); + const { org, isLoading: orgLoading } = useOrg(); + const navigate = useNavigate(); + // const [searchParams] = useSearchParams(); + // const token = searchParams.get("token"); - const [searchParams] = useSearchParams(); - const token = searchParams.get("token"); - - const newPath = token - ? `/sandbox/onboarding?token=${token}` - : "/sandbox/onboarding"; - const callbackPath = token - ? `/sandbox/onboarding?token=${token}` - : "/customers"; + const newPath = "/sandbox/onboarding"; + const callbackPath = "/customers"; useEffect(() => { - if (session?.user) { - window.location.href = callbackPath; + if (org?.onboarded) { + navigate(callbackPath); } - }, [session]); + }, [org, navigate]); const handleEmailSignIn = async (e: React.FormEvent) => { e.preventDefault(); setSendOtpLoading(true); try { - const { data, error } = await authClient.emailOtp.sendVerificationOtp({ + const { error } = await authClient.emailOtp.sendVerificationOtp({ email: email, type: "sign-in", }); @@ -49,7 +48,7 @@ export const SignIn = () => { } else { setOtpSent(true); } - } catch (error) { + } catch { toast.error("Something went wrong. Please try again."); } finally { setSendOtpLoading(false); @@ -60,7 +59,7 @@ export const SignIn = () => { setGoogleLoading(true); try { const frontendUrl = import.meta.env.VITE_FRONTEND_URL; - const { data, error } = await signIn.social({ + const { error } = await signIn.social({ provider: "google", callbackURL: `${frontendUrl}${callbackPath}`, newUserCallbackURL: `${frontendUrl}${newPath}`, @@ -104,79 +103,58 @@ export const SignIn = () => { )} {!otpSent && ( - <> -
- {/* Google Sign In Button */} - +
+ {/* Google Sign In Button */} + - {/* */} - - {/* Divider */} -
-
- -
-
- - Or - -
+ {/* Divider */} +
+
+
- -
-
- setEmail(e.target.value)} - required - className="text-base" - autoComplete="email" - /> -
- - {/* Sign In Button */} - +
+ + Or +
- {/* Footer */} - {/*
- - Create an account here - -
*/} - +
+
+ setEmail(e.target.value)} + required + className="text-base" + autoComplete="email" + /> +
+ + {/* Sign In Button */} + +
+
)}
diff --git a/vite/src/views/auth/components/OTPSignIn.tsx b/vite/src/views/auth/components/OTPSignIn.tsx index 6792f79f4..d612e01cb 100644 --- a/vite/src/views/auth/components/OTPSignIn.tsx +++ b/vite/src/views/auth/components/OTPSignIn.tsx @@ -1,3 +1,7 @@ +import { differenceInSeconds } from "date-fns"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router"; +import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { InputOTP, @@ -7,10 +11,6 @@ import { } from "@/components/ui/input-otp"; import { authClient } from "@/lib/auth-client"; import { cn } from "@/lib/utils"; -import { differenceInSeconds } from "date-fns"; -import { useEffect, useState } from "react"; -import { useNavigate } from "react-router"; -import { toast } from "sonner"; export const OTPSignIn = ({ email, @@ -35,15 +35,13 @@ export const OTPSignIn = ({ const handleSubmit = async (otp: string) => { setVerifying(true); + try { const { data, error } = await authClient.signIn.emailOtp({ email: email, otp: otp, }); - console.log("Data", data); - console.log("Error", error); - if (error) { toast.error(error.message || "Failed to verify code"); setVerifying(false); @@ -52,17 +50,25 @@ export const OTPSignIn = ({ const user = data.user; - const createdRecently = - differenceInSeconds(new Date(), new Date(user.createdAt)) < 20; + console.log("Data:", data); + + // Ensure we're comparing UTC timestamps + + const userCreatedAtUTC = new Date(user.createdAt); + const nowUTC = new Date(); + const diffSeconds = differenceInSeconds(nowUTC, userCreatedAtUTC); + + const createdRecently = diffSeconds < 20; if (createdRecently) { window.location.href = newPath; } else { window.location.href = callbackPath; } - } catch (error) { + } catch { toast.error("Failed to verify code"); } + console.log("OTP verified"); setVerifying(false); }; diff --git a/vite/src/views/command-bar/CommandBar.tsx b/vite/src/views/command-bar/CommandBar.tsx index 1e825373e..83f895238 100644 --- a/vite/src/views/command-bar/CommandBar.tsx +++ b/vite/src/views/command-bar/CommandBar.tsx @@ -294,7 +294,7 @@ const CommandBar = () => { const navigationItems = [ { - title: "Go to Products", + title: "Go to Plans", icon: , shortcutKey: "1", onSelect: () => { @@ -627,7 +627,7 @@ const CommandBar = () => { { - let stripeConnected = org?.stripe_connected; + // let stripeConnected = org?.stripe_connected; - if (!stripeConnected) { - const { data: org } = await OrgService.get(axiosInstance); - stripeConnected = org?.stripe_connected; - } + // if (!stripeConnected) { + // const { data: org } = await OrgService.get(axiosInstance); + // stripeConnected = org?.stripe_connected; + // } - if (!stripeConnected) { - toast.error("Connect to Stripe to add products to customers"); - const redirectUrl = getRedirectUrl(`/customers/${customer.id}`, env); - navigateTo(`/dev?tab=stripe`, navigate, env); - return; - } + // if (!stripeConnected) { + // toast.error("Connect to Stripe to add products to customers"); + // const redirectUrl = getRedirectUrl(`/customers/${customer.id}`, env); + // navigateTo(`/dev?tab=stripe`, navigate, env); + // return; + // } navigateTo( `/customers/${customer.id || customer.internal_id}/${productId}${ @@ -99,7 +97,7 @@ function AttachProductDropdown({ setSearchQuery(e.target.value)} diff --git a/vite/src/views/customers/customer/customer-product-list/CancelProductDialog.tsx b/vite/src/views/customers/customer/customer-product-list/CancelProductDialog.tsx index 64ec7cd86..c2a784b7e 100644 --- a/vite/src/views/customers/customer/customer-product-list/CancelProductDialog.tsx +++ b/vite/src/views/customers/customer/customer-product-list/CancelProductDialog.tsx @@ -50,9 +50,9 @@ export const CancelProductDialog = ({ }); await refetch(); setOpen(false); - toast.success("Product cancelled"); + toast.success("Plan cancelled"); } catch (error) { - toast.error(getBackendErr(error, "Failed to cancel product")); + toast.error(getBackendErr(error, "Failed to cancel plan")); } finally { if (cancelImmediately) { setImmediateLoading(false); diff --git a/vite/src/views/customers/customer/customer-product-list/TransferProductDialog.tsx b/vite/src/views/customers/customer/customer-product-list/TransferProductDialog.tsx index fbdba8187..3d1ba5d5f 100644 --- a/vite/src/views/customers/customer/customer-product-list/TransferProductDialog.tsx +++ b/vite/src/views/customers/customer/customer-product-list/TransferProductDialog.tsx @@ -47,7 +47,7 @@ export const TransferProductDialog = ({ const handleClicked = async () => { if (!selectedEntity) { - toast.error("Please select an entity to transfer the product to"); + toast.error("Please select an entity to transfer the plan to"); return; } @@ -68,11 +68,11 @@ export const TransferProductDialog = ({ }, ); await refetch(); - toast.success("Product transferred successfully"); + toast.success("Plan transferred successfully"); setOpen(false); } catch (error) { console.log(error); - toast.error(getBackendErr(error, "Failed to transfer product")); + toast.error(getBackendErr(error, "Failed to transfer plan")); } setLoading(false); }; diff --git a/vite/src/views/customers/customer/hooks/useCusQuery.tsx b/vite/src/views/customers/customer/hooks/useCusQuery.tsx index 6d7ca7ef0..daff19ec8 100644 --- a/vite/src/views/customers/customer/hooks/useCusQuery.tsx +++ b/vite/src/views/customers/customer/hooks/useCusQuery.tsx @@ -1,12 +1,13 @@ +import { useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; +import { useParams } from "react-router"; import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { useQuery } from "@tanstack/react-query"; -import { useParams } from "react-router"; +import { throwBackendError } from "@/utils/genUtils"; import { useCachedCustomer } from "./useCachedCustomer"; -import { useMemo } from "react"; -export const useCusQuery = () => { +export const useCusQuery = ({ enabled = true }: { enabled?: boolean } = {}) => { const { customer_id } = useParams(); const axiosInstance = useAxiosInstance(); const { getCachedCustomer } = useCachedCustomer(customer_id); @@ -18,7 +19,7 @@ export const useCusQuery = () => { const { data } = await axiosInstance.get(`/customers/${customer_id}`); return data; } catch (error) { - return null; + throwBackendError(error); } }; @@ -30,6 +31,8 @@ export const useCusQuery = () => { } = useQuery({ queryKey: ["customer", customer_id], queryFn: fetcher, + enabled: enabled && !!customer_id, + retry: false, }); const { products, isLoading: productsLoading } = useProductsQuery(); diff --git a/vite/src/views/customers/customer/hooks/useCusReferralQuery.tsx b/vite/src/views/customers/customer/hooks/useCusReferralQuery.tsx index 280126624..bb2ad042f 100644 --- a/vite/src/views/customers/customer/hooks/useCusReferralQuery.tsx +++ b/vite/src/views/customers/customer/hooks/useCusReferralQuery.tsx @@ -1,6 +1,6 @@ -import { useAxiosInstance } from "@/services/useAxiosInstance"; import { useQuery } from "@tanstack/react-query"; import { useParams } from "react-router"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; export const useCusReferralQuery = () => { const { customer_id } = useParams(); @@ -23,6 +23,7 @@ export const useCusReferralQuery = () => { } = useQuery({ queryKey: ["customer_referrals", customer_id], queryFn: referralFetcher, + retry: false, }); return { diff --git a/vite/src/views/customers/customer/product/components/AttachButton.tsx b/vite/src/views/customers/customer/product/components/AttachButton.tsx index 8bab58054..6f385b631 100644 --- a/vite/src/views/customers/customer/product/components/AttachButton.tsx +++ b/vite/src/views/customers/customer/product/components/AttachButton.tsx @@ -40,7 +40,7 @@ export const AttachButton = () => { setOpen(true); } catch (error) { console.log("error", error); - toast.error(getBackendErr(error, "Failed to attach product")); + toast.error(getBackendErr(error, "Failed to attach plan")); } setButtonLoading(false); diff --git a/vite/src/views/customers/customer/product/components/AttachModal.tsx b/vite/src/views/customers/customer/product/components/AttachModal.tsx index 6fa829c7c..db194808c 100644 --- a/vite/src/views/customers/customer/product/components/AttachModal.tsx +++ b/vite/src/views/customers/customer/product/components/AttachModal.tsx @@ -110,7 +110,7 @@ export const AttachModal = ({ } if (flags.isCanceled) { - return "Renew Product"; + return "Renew Plan"; } if (preview?.func === AttachFunction.CreateCheckout) { @@ -182,7 +182,7 @@ export const AttachModal = ({ } navigateTo(`/customers/${cusId}`, navigation, env); - toast.success(data.message || "Successfully attached product"); + toast.success(data.message || "Successfully attached plan"); setOpen(false); } catch (error) { console.log("Error creating product: ", error); @@ -197,9 +197,9 @@ export const AttachModal = ({ env, ); } else { - toast.error(getBackendErr(error, "Error creating product")); + toast.error(getBackendErr(error, "Error creating plan")); } - } finally { + } finally{ setLoading(false); } }; @@ -217,7 +217,7 @@ export const AttachModal = ({ > - Attach product + Attach plan @@ -226,7 +226,7 @@ export const AttachModal = ({

Details

- Product + Plan {product?.name} diff --git a/vite/src/views/customers/customer/product/components/InvoiceCustomerButton.tsx b/vite/src/views/customers/customer/product/components/InvoiceCustomerButton.tsx index 834dbec06..62268a862 100644 --- a/vite/src/views/customers/customer/product/components/InvoiceCustomerButton.tsx +++ b/vite/src/views/customers/customer/product/components/InvoiceCustomerButton.tsx @@ -43,7 +43,7 @@ export const InvoiceCustomerButton = ({ endIcon={} disableStartIcon={true} tabIndex={-1} - tooltipContent="This will enable the product for the customer immediately, and redirect you to Stripe to finalize the invoice" + tooltipContent="This will enable the plan for the customer immediately, and redirect you to Stripe to finalize the invoice" disabled={disabled} > Invoice Customer @@ -52,9 +52,9 @@ export const InvoiceCustomerButton = ({
-

Enable Product Immediately

+

Enable Plan Immediately

- This will enable the product for the customer immediately, and + This will enable the plan for the customer immediately, and redirect you to Stripe to finalize the invoice

diff --git a/vite/src/views/developer/configure-stripe/ConfigureStripe.tsx b/vite/src/views/developer/configure-stripe/ConfigureStripe.tsx index 8077a9477..78b8d6a34 100644 --- a/vite/src/views/developer/configure-stripe/ConfigureStripe.tsx +++ b/vite/src/views/developer/configure-stripe/ConfigureStripe.tsx @@ -1,47 +1,78 @@ -import FieldLabel from "@/components/general/modal-components/FieldLabel"; -import { InfoTooltip } from "@/components/general/modal-components/InfoTooltip"; +import { AppEnv } from "@autumn/shared"; +import { useEffect, useState } from "react"; +import { useSearchParams } from "react-router"; +import { toast } from "sonner"; import { PageSectionHeader } from "@/components/general/PageSectionHeader"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Button } from "@/components/v2/buttons/Button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/v2/cards/Card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/v2/dialogs/Dialog"; +import { FormLabel } from "@/components/v2/form/FormLabel"; +import { Input } from "@/components/v2/inputs/Input"; +import { CurrencySelect } from "@/components/v2/selects/CurrencySelect"; +import { useAutumnFlags } from "@/hooks/common/useAutumnFlags"; import { useOrg } from "@/hooks/common/useOrg"; +import { useOrgStripeQuery } from "@/hooks/queries/useOrgStripeQuery"; import { OrgService } from "@/services/OrgService"; import { useAxiosInstance } from "@/services/useAxiosInstance"; -import { getBackendErr } from "@/utils/genUtils"; -import LoadingScreen from "@/views/general/LoadingScreen"; -import { CurrencySelect } from "@/views/onboarding/ConnectStripe"; -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"; +import { getBackendErr } from "@/utils/genUtils"; +import { getStripeDashboardLink } from "@/utils/linkUtils"; +import ConnectStripeDialog from "@/views/onboarding2/ConnectStripeDialog"; +import { DisconnectStripePopover } from "./DisconnectStripePopover"; export const ConfigureStripe = () => { - const env = useEnv(); - const { org, isLoading, mutate } = useOrg(); + const { org, mutate } = useOrg(); + const { stripeAccount, isLoading: isLoadingStripeAccount } = + useOrgStripeQuery(); const axiosInstance = useAxiosInstance(); + const [searchParams, setSearchParams] = useSearchParams(); + const flags = useAutumnFlags(); - const [newStripeConfig, setNewStripeConfig] = useState({ + const [newStripeConfig, setNewStripeConfig] = useState({ success_url: org?.success_url, default_currency: org?.default_currency, - secret_key: org?.stripe_connected ? "Stripe connected" : "", }); const [connecting, setConnecting] = useState(false); + const [showConnectDialog, setShowConnectDialog] = useState(false); + const [showDuplicateDialog, setShowDuplicateDialog] = useState(false); + const env = useEnv(); + + // Check if user can paste secret keys (feature flagged) + const canPasteSecretKey = + flags.stripe_key === true || flags.platform === true; useEffect(() => { setNewStripeConfig({ success_url: org?.success_url, default_currency: org?.default_currency, - stripe_connected: org?.stripe_connected, }); }, [org]); + useEffect(() => { + const error = searchParams.get("error"); + if (error === "account_already_connected") { + setShowDuplicateDialog(true); + } + }, [searchParams]); + const allowSave = () => { return ( newStripeConfig.success_url !== org?.success_url || - newStripeConfig.default_currency !== org?.default_currency || - (!org?.stripe_connected && !!newStripeConfig.secret_key) + newStripeConfig.default_currency !== org?.default_currency ); }; @@ -64,16 +95,161 @@ export const ConfigureStripe = () => { } }; - if (isLoading) return ; + const handleRedirectToOAuth = async () => { + try { + const { data } = await axiosInstance.get( + `/v1/organization/stripe/oauth_url`, + ); + window.open(data.oauth_url, "_blank"); + } catch (error) { + toast.error(getBackendErr(error, "Failed to redirect to OAuth")); + } + }; + + const getConnectionStatus = () => { + const connection = org?.stripe_connection; + const accountName = + stripeAccount?.business_profile?.name || + stripeAccount?.settings?.dashboard?.display_name; + const accountId = stripeAccount?.id; + + if (connection === "secret_key") { + return { + description: `You have connected the Stripe account ${accountId}${accountName ? ` (${accountName})` : ""} via secret key.`, // Will show dashboard link in the same line + showDisconnect: true, + showConnectButtons: false, + showDefaultAccountLink: true, + }; + } + + if (connection === "oauth") { + const accountName = + stripeAccount?.business_profile?.name || + stripeAccount?.settings?.dashboard?.display_name; + const accountId = stripeAccount?.id; + return { + description: `You have connected the Stripe account ${accountId}${accountName ? ` (${accountName})` : ""} via OAuth.`, + showDisconnect: true, + showConnectButtons: false, + showDefaultAccountLink: false, + }; + } + + if (connection === "default") { + return { + description: + env === AppEnv.Live + ? "To start taking payments in Production, connect your Stripe live account below:" + : "You are using Autumn's default test account. To connect your own, click the button below", + showDisconnect: false, + showConnectButtons: true, + showDefaultAccountLink: false, // Don't show for default accounts + }; + } + + return { + description: + env === AppEnv.Live + ? "To start taking payments in Production, connect your Stripe live account below:" + : "No Stripe account connected", + showDisconnect: false, + showConnectButtons: true, + showDefaultAccountLink: false, + }; + }; + + const getDashboardUrl = () => { + const connection = org?.stripe_connection; + + if (connection === "oauth" && stripeAccount?.id) { + return getStripeDashboardLink({ + env, + accountId: stripeAccount?.id, + }); + } + + // For secret_key, link to main dashboard (no account ID) + if (connection === "secret_key") { + return getStripeDashboardLink({ + env, + accountId: stripeAccount?.id, + }); + } + + return null; + }; + + const status = getConnectionStatus(); + const dashboardUrl = getDashboardUrl(); return (
+ + + Connect your Stripe account + {isLoadingStripeAccount ? ( +
+ + +
+ ) : ( + status.description && ( + + {status.description} + {dashboardUrl && ( + + {" "} + Visit the Stripe dashboard{" "} + + here + + + )} + + ) + )} +
+ + +
+ {status.showConnectButtons && ( + <> + + {canPasteSecretKey && ( + + )} + + )} + + {status.showDisconnect && ( + { + await mutate(); + }} + /> + )} +
+
+
+
- + Success URL - +

This will be the default URL that users are redirected to after a successful checkout session. It can be overriden through the API. @@ -91,14 +267,13 @@ export const ConfigureStripe = () => {

- + Default Currency - +

This currency that your prices will be created in. This setting is shared between your sandbox and production environment.

- {/* */} @@ -109,67 +284,7 @@ export const ConfigureStripe = () => { } />
-
- - Stripe Secret Key - -

- You can retrieve this from your Stripe dashboard{" "} - - here - - . -

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

The following scopes are needed:

-
    -
  • Core (read & write)
  • -
  • Checkout (read & write)
  • -
  • Billing (read & write)
  • -
  • All webhooks (write)
  • -
  • Connect → Account Links (write)
  • -
-

- In your Stripe dashboard, go to Developers → API keys, click {" "} - Create restricted key, and enable the scopes above with the - listed permissions. -

-
-
-
- )} - - {org.stripe_connected ? ( - } - /> - ) : ( - - setNewStripeConfig({ - ...newStripeConfig, - secret_key: e.target.value, - }) - } - /> - )} -
- {org.stripe_connected ? ( - { - await mutate(); - setNewStripeConfig({ - ...newStripeConfig, - secret_key: "", - }); - }} - /> - ) : ( -
- )}
+ + + + { + setShowDuplicateDialog(open); + if (!open) { + // Clear query params when closing dialog + searchParams.delete("error"); + searchParams.delete("account_id"); + searchParams.delete("account_name"); + searchParams.delete("connected_org_name"); + searchParams.delete("connected_org_slug"); + setSearchParams(searchParams); + } + }} + > + + + Account Already Connected + + The Stripe account{" "} + {searchParams.get("account_id")} + {searchParams.get("account_name") && ( + <> ({searchParams.get("account_name")}) + )}{" "} + is already connected to the Autumn organization{" "} + {searchParams.get("connected_org_name")} + {searchParams.get("connected_org_slug") && ( + <> ({searchParams.get("connected_org_slug")}) + )} + . Please disconnect it from there first before connecting to this + organization. + + + + +
); }; diff --git a/vite/src/views/developer/configure-stripe/DisconnectStripePopover.tsx b/vite/src/views/developer/configure-stripe/DisconnectStripePopover.tsx index 664a2b5a7..cae71eae8 100644 --- a/vite/src/views/developer/configure-stripe/DisconnectStripePopover.tsx +++ b/vite/src/views/developer/configure-stripe/DisconnectStripePopover.tsx @@ -1,17 +1,15 @@ -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; +import { useState } from "react"; +import { toast } from "sonner"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { useOrg } from "@/hooks/common/useOrg"; -import { useListOrganizations } from "@/lib/auth-client"; +import { Button } from "@/components/v2/buttons/Button"; +import { Input } from "@/components/v2/inputs/Input"; import { OrgService } from "@/services/OrgService"; import { useAxiosInstance } from "@/services/useAxiosInstance"; import { getBackendErr } from "@/utils/genUtils"; -import { useState } from "react"; -import { toast } from "sonner"; export const DisconnectStripePopover = ({ onSuccess, @@ -46,9 +44,7 @@ export const DisconnectStripePopover = ({ return ( - +
@@ -62,7 +58,7 @@ export const DisconnectStripePopover = ({ onChange={(e) => setConfirmText(e.target.value)} /> - - - - - No currency found. - - - {stripeCurrencyCodes.map((currency) => ( - { - setDefaultCurrency(value); - setOpen(false); - }} - className="p-2 flex items-center justify-between" - > - {currency.currency} - {currency.code} - - - ))} - - - - - + ); }; diff --git a/vite/src/views/onboarding/onboarding-steps/AttachProduct.tsx b/vite/src/views/onboarding/onboarding-steps/AttachProduct.tsx index 0d367d884..6d1922425 100644 --- a/vite/src/views/onboarding/onboarding-steps/AttachProduct.tsx +++ b/vite/src/views/onboarding/onboarding-steps/AttachProduct.tsx @@ -72,7 +72,7 @@ export default function AttachProduct({ onValueChange={setSelectedProductId} > - + {products.map((product) => ( diff --git a/vite/src/views/onboarding/onboarding-steps/CheckAccess.tsx b/vite/src/views/onboarding/onboarding-steps/CheckAccess.tsx index c4335c683..3060e87ae 100644 --- a/vite/src/views/onboarding/onboarding-steps/CheckAccess.tsx +++ b/vite/src/views/onboarding/onboarding-steps/CheckAccess.tsx @@ -134,7 +134,7 @@ export default function CheckAccessStep({
Check whether a customer can access a{" "} - {isProduct ? "product" : "feature"} by calling the{" "} + {isProduct ? "plan" : "feature"} by calling the{" "} ) : ( )} diff --git a/vite/src/views/onboarding/onboarding-steps/ProductList.tsx b/vite/src/views/onboarding/onboarding-steps/ProductList.tsx index 4346e0858..9ce9c7239 100644 --- a/vite/src/views/onboarding/onboarding-steps/ProductList.tsx +++ b/vite/src/views/onboarding/onboarding-steps/ProductList.tsx @@ -70,8 +70,8 @@ export const ProductList = ({ 0 - ? "Your products" - : "Create your products" + ? "Your plans" + : "Create your plans" } number={1} description={ @@ -104,7 +104,7 @@ export const ProductList = ({ }} > @@ -189,11 +189,11 @@ export const EditProductDialog = ({ product.id, product, ); - toast.success("Product updated successfully"); + toast.success("Plan updated successfully"); await mutate(); setOpen(false); } catch (error) { - toast.error(getBackendErr(error, "Failed to update product")); + toast.error(getBackendErr(error, "Failed to update plan")); } setCreateProductLoading(false); }; diff --git a/vite/src/views/onboarding2/AttachProduct.tsx b/vite/src/views/onboarding2/AttachProduct.tsx index 8f51ee2a1..aea69f2f4 100644 --- a/vite/src/views/onboarding2/AttachProduct.tsx +++ b/vite/src/views/onboarding2/AttachProduct.tsx @@ -70,7 +70,7 @@ export default function AttachProduct({ onValueChange={setSelectedProductId} > - + {products.map((product) => ( diff --git a/vite/src/views/onboarding2/ConnectStripeDialog.tsx b/vite/src/views/onboarding2/ConnectStripeDialog.tsx index b7baef266..bb20338af 100644 --- a/vite/src/views/onboarding2/ConnectStripeDialog.tsx +++ b/vite/src/views/onboarding2/ConnectStripeDialog.tsx @@ -1,17 +1,18 @@ -import { Dialog, DialogHeader, DialogTitle } from "@/components/ui/dialog"; -import { useModelPricingContext } from "./model-pricing/ModelPricingContext"; - -import { - CustomDialogBody, - CustomDialogContent, - CustomDialogFooter, -} from "@/components/general/modal-components/DialogContentWrapper"; -import { Button } from "@/components/ui/button"; import { useState } from "react"; -import { Input } from "@/components/ui/input"; -import { connectStripe } from "./utils/connectStripe"; -import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { InfoTooltip } from "@/components/general/modal-components/InfoTooltip"; +import { Button } from "@/components/v2/buttons/Button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/v2/dialogs/Dialog"; +import { Input } from "@/components/v2/inputs/Input"; import { useOrg } from "@/hooks/common/useOrg"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { connectStripe } from "./utils/connectStripe"; export default function ConnectStripeDialog({ open, @@ -35,41 +36,51 @@ export default function ConnectStripeDialog({ return ( - - - - Connect your Stripe account - -

- To add a product to a customer, first connect your Stripe account. - Grab your secret key{" "} - - here - -

- {/* */} - setTestApiKey(e.target.value)} - disabled={org?.stripe_connected} - /> - - - - - + + ); } diff --git a/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx b/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx index 427c1a913..5cc0840d8 100644 --- a/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx +++ b/vite/src/views/onboarding2/integrate/IntegrateAutumn.tsx @@ -13,7 +13,6 @@ import { ArrowLeftIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { NextSteps } from "./NextSteps"; import { AutumnProvider } from "autumn-js/react"; -import { ConnectStripeStep } from "./ConnectStripeStep"; import { useOnboardingQueryState } from "../hooks/useOnboardingQueryState"; export default function IntegrateAutumn() { @@ -50,7 +49,6 @@ export default function IntegrateAutumn() { {stackSelected && queryStates.reactTypescript && ( <> - diff --git a/vite/src/views/onboarding2/integrate/integration-steps/AddAutumnProvider.tsx b/vite/src/views/onboarding2/integrate/integration-steps/AddAutumnProvider.tsx index bbb08b438..25af76fb1 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/AddAutumnProvider.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/AddAutumnProvider.tsx @@ -113,7 +113,7 @@ export const AddAutumnProvider = () => { return (
Wrap your React app in {""} diff --git a/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx b/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx index 7d8330d31..5a1b85c18 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/AutumnHandler.tsx @@ -154,7 +154,7 @@ export const AutumnHandler = () => { return (
Mount autumnHandler to your backend diff --git a/vite/src/views/onboarding2/integrate/integration-steps/CheckoutPricingTable.tsx b/vite/src/views/onboarding2/integrate/integration-steps/CheckoutPricingTable.tsx index 7c36e0f66..3aeca0d45 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/CheckoutPricingTable.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/CheckoutPricingTable.tsx @@ -27,7 +27,7 @@ export const CheckoutPricingTable = () => { return (
Drop in {""} diff --git a/vite/src/views/onboarding2/integrate/integration-steps/EnvStep.tsx b/vite/src/views/onboarding2/integrate/integration-steps/EnvStep.tsx index b9934ecbd..60214406d 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/EnvStep.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/EnvStep.tsx @@ -10,7 +10,7 @@ export const EnvStep = () => { <>
Add the Autumn secret key to your {".env"}{" "} diff --git a/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx b/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx index db4e53cfd..0d1ed15e9 100644 --- a/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx +++ b/vite/src/views/onboarding2/integrate/integration-steps/Install.tsx @@ -9,7 +9,7 @@ const installCodeBun = `bun add autumn-js`; export const Install = () => { return (
- + { setProduct(newProduct); setOpen(false); } catch (error) { - toast.error(getBackendErr(error, "Failed to create product")); + toast.error(getBackendErr(error, "Failed to create plan")); } finally { setCreating(false); } diff --git a/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx b/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx index de9ae6f9e..eff04fb52 100644 --- a/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx +++ b/vite/src/views/onboarding2/model-pricing/edit-product/EditProductDetails.tsx @@ -46,7 +46,7 @@ export const EditProductDetails = () => { }); await refetch(); } catch (error) { - toast.error(getBackendErr(error, "Failed to create product")); + toast.error(getBackendErr(error, "Failed to create plan")); } finally { setCreateLoading(false); } diff --git a/vite/src/views/onboarding3/OnboardingPreview.tsx b/vite/src/views/onboarding3/OnboardingPreview.tsx index 33f26b631..b92135d67 100644 --- a/vite/src/views/onboarding3/OnboardingPreview.tsx +++ b/vite/src/views/onboarding3/OnboardingPreview.tsx @@ -9,6 +9,7 @@ import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; import { useFeatureStore } from "@/hooks/stores/useFeatureStore"; import { useProductStore } from "@/hooks/stores/useProductStore"; import { useIsEditingPlan, useSheetStore } from "@/hooks/stores/useSheetStore"; +import { cn } from "@/lib/utils"; import { keyToTitle } from "@/utils/formatUtils/formatTextUtils"; import { PlanCardToolbar } from "../products/plan/components/plan-card/PlanCardToolbar"; import { PlanFeatureList } from "../products/plan/components/plan-card/PlanFeatureList"; @@ -17,6 +18,7 @@ import { useOnboarding3QueryState } from "./hooks/useOnboarding3QueryState"; import { useOnboardingStore } from "./store/useOnboardingStore"; import { getStepNumber } from "./utils/onboardingUtils"; +const MAX_PLAN_NAME_LENGTH = 20; interface OnboardingPreviewProps { setConnectStripeOpen?: (open: boolean) => void; } @@ -79,45 +81,65 @@ export const OnboardingPreview = ({ } return ( - - -
-
- {showBasicInfo && product?.name ? ( - {product.name} - ) : ( - - Name your product - - )} + + + {/* Absolutely positioned toolbar - CANNOT MOVE */} + {showToolbar && ( +
+ +
+ )} - {playgroundMode === "edit" && product && ( - + {/* Left content with padding to avoid toolbar */} +
+
+ {showBasicInfo && product?.name ? ( +
+ + {product.name.length > MAX_PLAN_NAME_LENGTH + ? `${product.name.slice(0, MAX_PLAN_NAME_LENGTH)}...` + : product.name} + +
+ ) : ( +
+ Name your plan +
)}
-
- {showToolbar && ( - + MAX_PLAN_NAME_LENGTH - 10} /> - )} -
+
+ )}
{showPricing && ( } - className="mt-2 !opacity-100" + className="mt-2 !opacity-100 pointer-events-none" onClick={handleEdit} - disabled={isPlanBeingEdited} + disabled={true} > {basePrice?.amount ? ( @@ -134,7 +156,7 @@ export const OnboardingPreview = ({ {showDummyFeature && feature && ( <> - + )} diff --git a/vite/src/views/onboarding3/OnboardingView3.tsx b/vite/src/views/onboarding3/OnboardingView3.tsx index 25b0bc347..f456b2cf8 100644 --- a/vite/src/views/onboarding3/OnboardingView3.tsx +++ b/vite/src/views/onboarding3/OnboardingView3.tsx @@ -1,8 +1,9 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { SheetContainer } from "@/components/v2/sheets/InlineSheet"; import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; import { cn } from "@/lib/utils"; +import { trackSignUp } from "@/utils/posthogTracking"; import { ProductContext } from "@/views/products/product/ProductContext"; import LoadingScreen from "../general/LoadingScreen"; import { SaveChangesBar } from "../products/plan/components/SaveChangesBar"; @@ -52,15 +53,15 @@ export default function OnboardingContent() { // Initialize onboarding logic and store handlers useOnboardingLogic(); + // Track sign-up event on first mount + useEffect(() => { + trackSignUp(); + }, []); + // Compute loading state const isQueryLoading = productsLoading || featuresLoading; if (isQueryLoading || isCheckingAutoSkip) { - console.log( - "Rendering onboarding loader", - isQueryLoading, - isCheckingAutoSkip, - ); return ; } diff --git a/vite/src/views/onboarding3/components/DummyFeatureRow.tsx b/vite/src/views/onboarding3/components/DummyFeatureRow.tsx index 129c3bcb2..fce432f19 100644 --- a/vite/src/views/onboarding3/components/DummyFeatureRow.tsx +++ b/vite/src/views/onboarding3/components/DummyFeatureRow.tsx @@ -17,23 +17,28 @@ interface DummyFeatureRowProps { export const DummyFeatureRow = ({ feature }: DummyFeatureRowProps) => { // Map FeatureType to ProductItemFeatureType for icon display + // Using the same logic as getItemFeatureType from shared utils const getFeatureTypeForIcon = ( - featureType: FeatureType | null, + feature: CreateFeature, ): ProductItemFeatureType => { - if (featureType === FeatureType.Boolean) { - return ProductItemFeatureType.Boolean; + if (feature.type === FeatureType.Boolean) { + return ProductItemFeatureType.Static; } - if (featureType === FeatureType.Metered) { + if (feature.type === FeatureType.CreditSystem) { return ProductItemFeatureType.SingleUse; } - // Default to SingleUse for other types + // For Metered features, use the config's usage_type if available + if (feature.type === FeatureType.Metered && feature.config?.usage_type) { + return feature.config.usage_type as ProductItemFeatureType; + } + // Default to SingleUse return ProductItemFeatureType.SingleUse; }; // Create a mock ProductItem for PlanFeatureIcon const mockItem: ProductItem = { feature_id: feature.id || "", - feature_type: getFeatureTypeForIcon(feature.type), + feature_type: getFeatureTypeForIcon(feature), included_usage: null, interval: null, price: null, diff --git a/vite/src/views/onboarding3/components/ExitButton.tsx b/vite/src/views/onboarding3/components/ExitButton.tsx index e02bbe39f..1bd1a353f 100644 --- a/vite/src/views/onboarding3/components/ExitButton.tsx +++ b/vite/src/views/onboarding3/components/ExitButton.tsx @@ -6,7 +6,8 @@ import { TooltipContent, TooltipTrigger, } from "@/components/v2/tooltips/Tooltip"; -import { useEnv } from "@/utils/envUtils"; +import { useOrg } from "@/hooks/common/useOrg"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; import { pushPage } from "@/utils/genUtils"; import { useOnboarding3QueryState } from "../hooks/useOnboarding3QueryState"; import { OnboardingStep } from "../utils/onboardingUtils"; @@ -17,7 +18,8 @@ interface ExitButtonProps { export function ExitButton({ position = "absolute" }: ExitButtonProps) { const navigate = useNavigate(); - const env = useEnv(); + const { org, mutate: mutateOrg } = useOrg(); + const axiosInstance = useAxiosInstance(); const { queryStates } = useOnboarding3QueryState(); const step = queryStates.step; @@ -26,7 +28,14 @@ export function ExitButton({ position = "absolute" }: ExitButtonProps) { return null; } - const handleExit = () => { + const handleExit = async () => { + if (!org?.onboarded) { + await axiosInstance.patch("/v1/organization", { + onboarded: true, + }); + await mutateOrg(); + } + pushPage({ navigate, path: "/products", diff --git a/vite/src/views/onboarding3/components/IntegrationStep.tsx b/vite/src/views/onboarding3/components/IntegrationStep.tsx index d6630f1c4..e677b6784 100644 --- a/vite/src/views/onboarding3/components/IntegrationStep.tsx +++ b/vite/src/views/onboarding3/components/IntegrationStep.tsx @@ -1,6 +1,5 @@ import { Separator } from "@/components/v2/separator"; import { BackendSection } from "./integration-step/BackendSection"; -import { ConnectStripeSection } from "./integration-step/ConnectStripeSection"; import { EnvSection } from "./integration-step/EnvSection"; import { FrontendSection } from "./integration-step/FrontendSection"; import { InstallSection } from "./integration-step/InstallSection"; @@ -24,8 +23,6 @@ export const IntegrationStep = () => { - - diff --git a/vite/src/views/onboarding3/components/OnboardingStepRenderer.tsx b/vite/src/views/onboarding3/components/OnboardingStepRenderer.tsx index 4a47d6a87..e84585142 100644 --- a/vite/src/views/onboarding3/components/OnboardingStepRenderer.tsx +++ b/vite/src/views/onboarding3/components/OnboardingStepRenderer.tsx @@ -34,6 +34,7 @@ export const OnboardingStepRenderer = () => { const sheetType = useSheetStore((s) => s.type); const itemId = useSheetStore((s) => s.itemId); const [trackResponse, setTrackResponse] = useState(null); + const [checkResponse, setCheckResponse] = useState(null); const [lastUsedFeatureId, setLastUsedFeatureId] = useState< string | undefined >(undefined); @@ -183,10 +184,12 @@ export const OnboardingStepRenderer = () => { <> diff --git a/vite/src/views/onboarding3/components/PlanDetailsStep.tsx b/vite/src/views/onboarding3/components/PlanDetailsStep.tsx index 830b03334..7124a5b93 100644 --- a/vite/src/views/onboarding3/components/PlanDetailsStep.tsx +++ b/vite/src/views/onboarding3/components/PlanDetailsStep.tsx @@ -23,7 +23,7 @@ export const PlanDetailsStep = () => { return ( <> - +
@@ -36,7 +36,7 @@ export const PlanDetailsStep = () => { /> - The display name of the product that will show up on your + The display name of the plan that will show up on your checkout page
@@ -49,8 +49,7 @@ export const PlanDetailsStep = () => { className="mb-1" /> - A fixed price to charge for the product. Uncheck this section if - the product is free or a variable price. + Used to refer to this plan when using Autumn's APIs or SDKs
{/* {step === OnboardingStep.Playground && product && ( diff --git a/vite/src/views/onboarding3/components/integration-step/ConnectStripeSection.tsx b/vite/src/views/onboarding3/components/integration-step/ConnectStripeSection.tsx index ddcfa6f14..11cf6215c 100644 --- a/vite/src/views/onboarding3/components/integration-step/ConnectStripeSection.tsx +++ b/vite/src/views/onboarding3/components/integration-step/ConnectStripeSection.tsx @@ -41,7 +41,7 @@ export const ConnectStripeSection = () => { title="Connect Stripe" description={ - Stripe is required to checkout and add your products to customers. + Stripe is required to checkout and add your plans to customers. Grab your API key{" "} void; + onCheckSuccess?: (response: any) => void; onFeatureUsed?: (featureId: string) => void; }) => { - const { customer, track, refetch } = useCustomer(); + const { customer, track, refetch, check } = useCustomer(); const { features } = useFeaturesQuery(); return ( @@ -101,6 +103,16 @@ export const AvailableFeatures = ({ handleSend={async (value) => { const featureId = customer?.features[x].id; + // Check the feature access + const { data: checkResponse, error: checkError } = + await check({ + featureId: featureId, + requiredBalance: value, + }); + + if (!checkError && checkResponse && onCheckSuccess) { + onCheckSuccess(checkResponse); + } // Notify parent which feature was used if (onFeatureUsed && featureId !== undefined) { onFeatureUsed(featureId); @@ -123,8 +135,8 @@ export const AvailableFeatures = ({ )) ) : ( - Your current product doesn't have any features. Try purchasing a - product in the preview first. + Your current plan doesn't have any features. Try purchasing a + plan in the preview first. )}
diff --git a/vite/src/views/onboarding3/components/playground-step/PlaygroundToolbar.tsx b/vite/src/views/onboarding3/components/playground-step/PlaygroundToolbar.tsx index d4fe83c30..4d70fd421 100644 --- a/vite/src/views/onboarding3/components/playground-step/PlaygroundToolbar.tsx +++ b/vite/src/views/onboarding3/components/playground-step/PlaygroundToolbar.tsx @@ -50,30 +50,43 @@ export const PlaygroundToolbar = () => { }, ]} /> -
- - -
+ {playgroundMode === "edit" && ( +
+ {products.filter((p) => !p.archived).length > 1 && ( + + )} + +
+ )}
); }; diff --git a/vite/src/views/onboarding3/components/playground-step/QuickStartCodeGroup.tsx b/vite/src/views/onboarding3/components/playground-step/QuickStartCodeGroup.tsx index 7fb203932..5867dfec3 100644 --- a/vite/src/views/onboarding3/components/playground-step/QuickStartCodeGroup.tsx +++ b/vite/src/views/onboarding3/components/playground-step/QuickStartCodeGroup.tsx @@ -1,5 +1,6 @@ import type { ProductItem } from "@autumn/shared"; -import type { TrackResult } from "autumn-js"; +import type { CheckResult, TrackResult } from "autumn-js"; +import { useCustomer } from "autumn-js/react"; import { useEffect, useState } from "react"; import { CodeGroup, @@ -10,7 +11,8 @@ import { CodeGroupTab, } from "@/components/v2/CodeGroup"; import { SheetSection } from "@/components/v2/sheets/InlineSheet"; -import { useProductContext } from "@/views/products/product/ProductContext"; +import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; +import { useProductStore } from "@/hooks/stores/useProductStore"; import { getCodeSnippets } from "../../utils/completionStepCode"; type CodeLanguage = "react" | "nodejs" | "response"; @@ -19,28 +21,34 @@ const CodeSnippetSection = ({ title, snippets, trackResponse, + checkResponse, }: { title: string; snippets: { react: string; nodejs: string; response: string }; trackResponse?: TrackResult; + checkResponse?: CheckResult; }) => { const [activeLanguage, setActiveLanguage] = useState("react"); - // Auto-switch to response tab when trackResponse is available (for track section only) + // Auto-switch to response tab when responses are available useEffect(() => { if (trackResponse && title === "Track usage") { setActiveLanguage("response"); } - }, [trackResponse, title]); + if (checkResponse && title === "Check feature access") { + setActiveLanguage("response"); + } + }, [trackResponse, checkResponse, title]); const getCodeForTab = () => { - // Use dynamic trackResponse for track section response tab - if ( - activeLanguage === "response" && - title === "Track usage" && - trackResponse - ) { - return JSON.stringify(trackResponse, null, 2); + // Use dynamic responses for response tabs + if (activeLanguage === "response") { + if (title === "Track usage" && trackResponse) { + return JSON.stringify(trackResponse, null, 2); + } + if (title === "Check feature access" && checkResponse) { + return JSON.stringify(checkResponse, null, 2); + } } return snippets[activeLanguage]; }; @@ -70,7 +78,35 @@ const CodeSnippetSection = ({ {title === "Track usage" && trackResponse ? JSON.stringify(trackResponse, null, 2) - : snippets.response} + : title === "Check feature access" && checkResponse + ? JSON.stringify(checkResponse, null, 2) + : snippets.response} + + + +
+ ); +}; + +const CustomerSection = () => { + const { customer } = useCustomer(); + const [activeTab, setActiveTab] = useState("customer"); + + return ( +
+

Customer

+ + + Customer Response + + navigator.clipboard.writeText(JSON.stringify(customer, null, 2)) + } + /> + + + + {JSON.stringify(customer, null, 2)} @@ -80,12 +116,15 @@ const CodeSnippetSection = ({ export const QuickStartCodeGroup = ({ trackResponse, + checkResponse, featureId: usedFeatureId, }: { trackResponse?: TrackResult; + checkResponse?: CheckResult; featureId?: string; }) => { - const { product } = useProductContext(); + const { product } = useProductStore(); + const { features } = useFeaturesQuery(); // Use the feature that was actually used (if available), otherwise fallback to first feature const firstFeatureItem = product?.items?.find( @@ -94,8 +133,11 @@ export const QuickStartCodeGroup = ({ const featureId = usedFeatureId || firstFeatureItem?.feature_id || undefined; const productId = product?.id || undefined; + // Get the actual feature name from features list + const featureName = features.find((f) => f.id === featureId)?.name; + // Generate snippets with actual IDs from onboarding - const snippets = getCodeSnippets(featureId, productId); + const snippets = getCodeSnippets(featureId, productId, featureName); return ( @@ -103,6 +145,7 @@ export const QuickStartCodeGroup = ({ +
); diff --git a/vite/src/views/onboarding3/hooks/actions/useFeatureConfigActions.tsx b/vite/src/views/onboarding3/hooks/actions/useFeatureConfigActions.tsx index e00cd2743..10a45f223 100644 --- a/vite/src/views/onboarding3/hooks/actions/useFeatureConfigActions.tsx +++ b/vite/src/views/onboarding3/hooks/actions/useFeatureConfigActions.tsx @@ -4,6 +4,7 @@ import { useProductsQuery } from "@/hooks/queries/useProductsQuery"; import { useHasChanges, useProductStore } from "@/hooks/stores/useProductStore"; import { useSheetStore } from "@/hooks/stores/useSheetStore"; import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { trackOnboardingFeatureConfigured } from "@/utils/posthogTracking"; import { updateProduct } from "@/views/products/product/utils/updateProduct"; export const useFeatureConfigActions = () => { @@ -40,6 +41,9 @@ export const useFeatureConfigActions = () => { if (!saved) return false; + // Track feature configuration completion + trackOnboardingFeatureConfigured(); + // Open edit-plan sheet after successful save setSheet({ type: "edit-plan" }); diff --git a/vite/src/views/onboarding3/hooks/actions/useFeatureCreationActions.tsx b/vite/src/views/onboarding3/hooks/actions/useFeatureCreationActions.tsx index 5764644e0..5cb7955d3 100644 --- a/vite/src/views/onboarding3/hooks/actions/useFeatureCreationActions.tsx +++ b/vite/src/views/onboarding3/hooks/actions/useFeatureCreationActions.tsx @@ -12,6 +12,7 @@ import { useProductStore } from "@/hooks/stores/useProductStore"; import { FeatureService } from "@/services/FeatureService"; import { useAxiosInstance } from "@/services/useAxiosInstance"; import { getBackendErr } from "@/utils/genUtils"; +import { trackOnboardingFeatureCreation } from "@/utils/posthogTracking"; export const useFeatureCreationActions = () => { const axiosInstance = useAxiosInstance(); @@ -75,6 +76,11 @@ export const useFeatureCreationActions = () => { }); updatedFeature = apiFeatureToDbFeature({ apiFeature: data }); toast.success(`Feature "${feature.name}" created successfully!`); + + // Track feature creation in onboarding + trackOnboardingFeatureCreation({ + featureType: feature.type, + }); } console.log("Updated feature", updatedFeature); diff --git a/vite/src/views/onboarding3/hooks/actions/usePlanDetailsActions.tsx b/vite/src/views/onboarding3/hooks/actions/usePlanDetailsActions.tsx index 8b985aeb1..c623e32ee 100644 --- a/vite/src/views/onboarding3/hooks/actions/usePlanDetailsActions.tsx +++ b/vite/src/views/onboarding3/hooks/actions/usePlanDetailsActions.tsx @@ -7,6 +7,8 @@ import { useProductStore, } from "@/hooks/stores/useProductStore"; import { useAxiosInstance } from "@/services/useAxiosInstance"; +import { trackOnboardingProductCreation } from "@/utils/posthogTracking"; +import { isPriceItem } from "@/utils/product/getItemType"; import { updateProduct } from "@/views/products/product/utils/updateProduct"; import { createProduct } from "../../utils/onboardingUtils"; @@ -38,10 +40,21 @@ export const usePlanDetailsActions = () => { product: product as ProductV2, onSuccess: async () => {}, }); - toast.success("Product updated successfully"); + toast.success("Plan updated successfully"); } else { // Create new product newProduct = await createProduct(product, axiosInstance); + + // Track product creation in Amplitude (only on creation, not update) + if (newProduct) { + const isPaid = + newProduct.items?.some((item) => isPriceItem(item)) ?? false; + const productType = isPaid ? "paid" : "free"; + + trackOnboardingProductCreation({ + productType, + }); + } } if (!newProduct) return false; diff --git a/vite/src/views/onboarding3/hooks/actions/useStepActions.tsx b/vite/src/views/onboarding3/hooks/actions/useStepActions.tsx index 95e3dfc20..f6454a2e2 100644 --- a/vite/src/views/onboarding3/hooks/actions/useStepActions.tsx +++ b/vite/src/views/onboarding3/hooks/actions/useStepActions.tsx @@ -6,10 +6,15 @@ import { } from "@autumn/shared"; import { useCallback } from "react"; import { useNavigate } from "react-router"; +import { useOrg } from "@/hooks/common/useOrg"; import { useFeatureStore } from "@/hooks/stores/useFeatureStore"; import { useHasChanges, useProductStore } from "@/hooks/stores/useProductStore"; -import { useEnv } from "@/utils/envUtils"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; import { pushPage } from "@/utils/genUtils"; +import { + trackOnboardingIntegrationCompleted, + trackOnboardingPlaygroundCompleted, +} from "@/utils/posthogTracking"; import { useOnboardingStore } from "../../store/useOnboardingStore"; import { getNextStep, OnboardingStep } from "../../utils/onboardingUtils"; import { useFeatureConfigActions } from "./useFeatureConfigActions"; @@ -38,7 +43,8 @@ export const useStepActions = (props: StepActionsProps) => { const { step, pushStep, validateStep, sharedActions } = props; const navigate = useNavigate(); - const env = useEnv(); + const { org, mutate: mutateOrg } = useOrg(); + const axiosInstance = useAxiosInstance(); // Get product from product store const product = useProductStore((s) => s.product); @@ -131,9 +137,25 @@ export const useStepActions = (props: StepActionsProps) => { if (!canProceed) return; if (nextStep) { + // Track playground completion when moving to integration + if (step === OnboardingStep.Playground) { + trackOnboardingPlaygroundCompleted(); + } + pushStep(nextStep); } else { + // Track integration completion + if (step === OnboardingStep.Integration) { + trackOnboardingIntegrationCompleted(); + } + // Finish onboarding + if (!org?.onboarded) { + await axiosInstance.patch("/v1/organization", { + onboarded: true, + }); + await mutateOrg(); + } pushPage({ navigate, @@ -161,6 +183,9 @@ export const useStepActions = (props: StepActionsProps) => { setIsButtonLoading, hasChanges, navigate, + org, + axiosInstance, + mutateOrg, ]); return { diff --git a/vite/src/views/onboarding3/hooks/useAutoSkipToPlayground.ts b/vite/src/views/onboarding3/hooks/useAutoSkipToPlayground.ts index 9d7084304..cff682376 100644 --- a/vite/src/views/onboarding3/hooks/useAutoSkipToPlayground.ts +++ b/vite/src/views/onboarding3/hooks/useAutoSkipToPlayground.ts @@ -69,20 +69,17 @@ export const useAutoSkipToPlayground = () => { hasFeature && product.items?.some( (item) => - item.feature_id && - !item.price_id && - features.some((f) => f.id === item.feature_id), + item.feature_id && features.some((f) => f.id === item.feature_id), ); - // If all conditions met, skip to playground - if (hasProduct && hasFeature && hasFeatureItem) { - setQueryStates({ step: OnboardingStep.Playground }); + if (hasProduct && hasFeature && !hasFeatureItem) { + setQueryStates({ step: OnboardingStep.FeatureConfiguration }); } setIsChecking(false); }, [ product?.id, - product?.items.some, + product?.items, features, featuresLoading, setQueryStates, diff --git a/vite/src/views/onboarding3/hooks/useOnboardingFeatureSync.tsx b/vite/src/views/onboarding3/hooks/useOnboardingFeatureSync.tsx index d1b8db232..fa28ae5b1 100644 --- a/vite/src/views/onboarding3/hooks/useOnboardingFeatureSync.tsx +++ b/vite/src/views/onboarding3/hooks/useOnboardingFeatureSync.tsx @@ -57,7 +57,6 @@ export const useOnboardingFeatureSync = () => { } // Step 3: Sort by metered features first - console.log("Candidate features before sorting:", candidateFeatures); candidateFeatures.sort((a, b) => { const aIsBoolean = a.type === FeatureType.Boolean; const bIsBoolean = b.type === FeatureType.Boolean; @@ -66,7 +65,6 @@ export const useOnboardingFeatureSync = () => { return 0; }); - console.log("Candidate features:", candidateFeatures); // Step 4: Take the first feature const featureToLoad = candidateFeatures[0]; diff --git a/vite/src/views/onboarding3/hooks/useOnboardingProductSync.ts b/vite/src/views/onboarding3/hooks/useOnboardingProductSync.ts index 5f669ac63..a2275ab85 100644 --- a/vite/src/views/onboarding3/hooks/useOnboardingProductSync.ts +++ b/vite/src/views/onboarding3/hooks/useOnboardingProductSync.ts @@ -45,8 +45,6 @@ export const useOnboardingProductSync = () => { } } - console.log("Selected product:", selectedProduct); - // Set both product and baseProduct setBaseProduct(selectedProduct); setProduct(selectedProduct); diff --git a/vite/src/views/onboarding3/utils/completionStepCode.ts b/vite/src/views/onboarding3/utils/completionStepCode.ts index 208afcccd..6ad79a1cc 100644 --- a/vite/src/views/onboarding3/utils/completionStepCode.ts +++ b/vite/src/views/onboarding3/utils/completionStepCode.ts @@ -1,6 +1,11 @@ -export const getCodeSnippets = (featureId?: string, productId?: string) => { +export const getCodeSnippets = ( + featureId?: string, + productId?: string, + featureName?: string, +) => { const actualFeatureId = featureId || "your_feature_id"; const actualProductId = productId || "your_product_id"; + const actualFeatureName = featureName || "Your Feature"; return { allowed: { @@ -27,7 +32,7 @@ const allowed = await autumn.check({ "allowed": true, "feature": { "id": "${actualFeatureId}", - "name": "Your Feature", + "name": "${actualFeatureName}", "type": "limit" } }`, @@ -73,7 +78,7 @@ console.log(session.checkout_url);`, "customer_id": "cust_123", "product": { "id": "${actualProductId}", - "name": "Your Product", + "name": "Your Plan", "items": [ { "type": "price", diff --git a/vite/src/views/onboarding3/utils/onboardingUtils.ts b/vite/src/views/onboarding3/utils/onboardingUtils.ts index 187655dae..fe008e7ee 100644 --- a/vite/src/views/onboarding3/utils/onboardingUtils.ts +++ b/vite/src/views/onboarding3/utils/onboardingUtils.ts @@ -61,19 +61,19 @@ export const getNextStep = ( // Step configuration for headers and descriptions export const stepConfig = { [OnboardingStep.PlanDetails]: { - title: "Create a product", + title: "Create a plan", description: - "Products are the pricing tiers your application offers. You can create your free tiers and your paid tiers too.", + "Plans are the pricing tiers your application offers. You can create your free tiers and your paid tiers too.", }, [OnboardingStep.FeatureCreation]: { title: "Create a feature", description: - "Create and add the first feature that customers on this plan get access to. One feature for each part of your app you want to gate based on pricing.", + "Features are the benefits customers get access to when using this plan. You can create a feature for things in your app you want to limit, track or bill for.", }, [OnboardingStep.FeatureConfiguration]: { title: "Define feature limits or billing", description: - "Features can be included as part of this product, or billed for based on their usage.", + "Features can be included as part of this plan, or billed for based on their usage.", }, [OnboardingStep.Playground]: { title: "Finish your setup", @@ -273,7 +273,7 @@ export const createProduct = async ( // created: true, // latestId: createdProduct.id, // }; - toast.success(`Product "${product?.name}" created successfully!`); + toast.success(`Plan "${product?.name}" created successfully!`); // if (!productCreatedRef.current.created) { // // First time creating the product @@ -352,12 +352,6 @@ export const createFeature = async ( // Product item creation helper export const createProductItem = (createdFeature: CreateFeature) => { - console.log("createProductItem - input feature:", { - id: createdFeature.id, - type: createdFeature.type, - config: createdFeature.config, - }); - // Map feature type to product item feature type let featureType: ProductItemFeatureType; @@ -387,8 +381,6 @@ export const createProductItem = (createdFeature: CreateFeature) => { featureType = ProductItemFeatureType.SingleUse; } - console.log("createProductItem - mapped to feature type:", featureType); - // Boolean features have a simplified structure with no pricing/billing properties if (createdFeature.type === FeatureType.Boolean) { return { diff --git a/vite/src/views/products/ProductConfig.tsx b/vite/src/views/products/ProductConfig.tsx index a8f469b81..ba1eb23c1 100644 --- a/vite/src/views/products/ProductConfig.tsx +++ b/vite/src/views/products/ProductConfig.tsx @@ -28,7 +28,7 @@ export const ProductConfig = ({
Name { setSource(e.target.value); @@ -40,7 +40,7 @@ export const ProductConfig = ({

- {keyToTitle(tab)} + {keyToTitle(tab, { exclusionMap: { products: "Plans" } })}

{tab === "products" && } diff --git a/vite/src/views/products/features/feature-row-toolbar/DeleteFeatureDialog.tsx b/vite/src/views/products/features/feature-row-toolbar/DeleteFeatureDialog.tsx index 6459a2185..6e4749b44 100644 --- a/vite/src/views/products/features/feature-row-toolbar/DeleteFeatureDialog.tsx +++ b/vite/src/views/products/features/feature-row-toolbar/DeleteFeatureDialog.tsx @@ -60,15 +60,15 @@ export const DeleteFeatureDialog = ({ const totalCount = Number.parseInt(deletionText.totalCount); if (Number.isNaN(totalCount) || totalCount <= 0) { - return "There are products using this feature. You must remove this feature from the products first, or archive it instead."; + return "There are plans using this feature. You must remove this feature from the plans first, or archive it instead."; } if (totalCount === 1) { - return `${deletionText.productName} is using this feature. You must remove this feature from the product first, or archive it instead.`; + return `${deletionText.productName} is using this feature. You must remove this feature from the plan first, or archive it instead.`; } const otherCount = totalCount - 1; - return `${deletionText.productName} and ${otherCount} other product${otherCount > 1 ? "s" : ""} are using this feature. You must remove this feature from the products first, or archive it instead.`; + return `${deletionText.productName} and ${otherCount} other plan${otherCount > 1 ? "s" : ""} are using this feature. You must remove this feature from the plans first, or archive it instead.`; } - return "There are products using this feature. You must remove this feature from the products first, or archive it instead."; + return "There are plans using this feature. You must remove this feature from the plans first, or archive it instead."; } return "Are you sure you want to delete this feature? This action cannot be undone."; }; diff --git a/vite/src/views/products/plan/PlanEditorView.tsx b/vite/src/views/products/plan/PlanEditorView.tsx index 5442c5e4a..d7d5ab1f1 100644 --- a/vite/src/views/products/plan/PlanEditorView.tsx +++ b/vite/src/views/products/plan/PlanEditorView.tsx @@ -1,5 +1,5 @@ import { AxiosError } from "axios"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import { useParams } from "react-router"; import { useFeaturesQuery } from "@/hooks/queries/useFeaturesQuery"; import { useProductSync } from "@/hooks/stores/useProductSync"; @@ -32,16 +32,20 @@ export default function PlanEditorView() { const [showNewVersionDialog, setShowNewVersionDialog] = useState(false); const setSheet = useSheetStore((s) => s.setSheet); + useEffect(() => { + setSheet({ type: "edit-plan" }); + }, [setSheet]); + if (featuresLoading || productLoading) return ; if (error || !originalProduct) { // Handle 500 errors from backend when product doesn't exist - let errorMessage = `Product ${product_id} not found`; + let errorMessage = `Plan ${product_id} not found`; if (error instanceof AxiosError && error.response?.status === 500) { - errorMessage = `Product ${product_id} not found`; + errorMessage = `Plan ${product_id} not found`; } else if (error) { - errorMessage = error.message || `Product ${product_id} not found`; + errorMessage = error.message || `Plan ${product_id} not found`; } return {errorMessage}; diff --git a/vite/src/views/products/plan/components/DeletePlanDialog.tsx b/vite/src/views/products/plan/components/DeletePlanDialog.tsx index 13c89a4c6..b0d1f3cd4 100644 --- a/vite/src/views/products/plan/components/DeletePlanDialog.tsx +++ b/vite/src/views/products/plan/components/DeletePlanDialog.tsx @@ -76,9 +76,9 @@ export const DeletePlanDialog = ({ } setOpen(false); - toast.success("Product deleted successfully"); + toast.success("Plan deleted successfully"); } catch (error: unknown) { - toast.error(getBackendErr(error as AxiosError, "Error deleting product")); + toast.error(getBackendErr(error as AxiosError, "Error deleting plan")); } finally { setLoading(false); } @@ -98,7 +98,7 @@ export const DeletePlanDialog = ({ setOpen(false); await Promise.all([invalidateProducts(), invalidateProduct()]); } catch (error) { - toast.error(getBackendErr(error, "Error archiving product")); + toast.error(getBackendErr(error, "Error archiving plan")); } finally { setLoading(false); } @@ -118,7 +118,7 @@ export const DeletePlanDialog = ({ toast.success(`${product.name} unarchived successfully`); setOpen(false); } catch (error) { - toast.error(getBackendErr(error, "Error unarchiving product")); + toast.error(getBackendErr(error, "Error unarchiving plan")); } finally { setLoading(false); } @@ -133,14 +133,14 @@ export const DeletePlanDialog = ({ const getDeleteMessage = () => { if (product.archived) { - return `Are you sure you want to unarchive ${product.name}? This will make it visible in your list of products.`; + return `Are you sure you want to unarchive ${product.name}? This will make it visible in your list of plans.`; } // \n\nNote: If there are multiple versions, this will unarchive all versions at once. const isMultipleVersions = productInfo?.numVersion > 1; - const versionText = deleteAllVersions ? "product" : "version"; - const productText = isMultipleVersions ? versionText : "product"; + const versionText = deleteAllVersions ? "plan" : "version"; + const productText = isMultipleVersions ? versionText : "plan"; const messageTemplates = { withCustomers: { @@ -151,9 +151,9 @@ export const DeletePlanDialog = ({ otherCount: number, productText: string, ) => - `${customerName} and ${otherCount} other customer${otherCount > 1 ? "s" : ""} are on this ${productText}. Are you sure you want to archive this product?`, + `${customerName} and ${otherCount} other customer${otherCount > 1 ? "s" : ""} are on this ${productText}. Are you sure you want to archive this plan?`, fallback: (productText: string) => - `There are customers on this ${productText}. Deleting this ${productText} will remove it from their accounts. Are you sure you want to continue? You can also archive the product instead.`, + `There are customers on this ${productText}. Deleting this ${productText} will remove it from their accounts. Are you sure you want to continue? You can also archive the plan instead.`, }, withoutCustomers: (productText: string) => `Are you sure you want to delete this ${productText}? This action cannot be undone.`, @@ -224,7 +224,7 @@ export const DeletePlanDialog = ({ Delete latest version - Archive product + Archive plan )} diff --git a/vite/src/views/products/plan/components/EditPlanSheet.tsx b/vite/src/views/products/plan/components/EditPlanSheet.tsx index bc80f02a8..b388e3332 100644 --- a/vite/src/views/products/plan/components/EditPlanSheet.tsx +++ b/vite/src/views/products/plan/components/EditPlanSheet.tsx @@ -14,7 +14,7 @@ export function EditPlanSheet({ isOnboarding }: { isOnboarding?: boolean }) { <> {!isOnboarding && ( )} diff --git a/vite/src/views/products/plan/components/ManagePlan.tsx b/vite/src/views/products/plan/components/ManagePlan.tsx index d561dc0a9..b3a66c643 100644 --- a/vite/src/views/products/plan/components/ManagePlan.tsx +++ b/vite/src/views/products/plan/components/ManagePlan.tsx @@ -2,7 +2,7 @@ import PlanCard from "./plan-card/PlanCard"; export const ManagePlan = () => { return ( -
+
); diff --git a/vite/src/views/products/plan/components/SaveChangesBar.tsx b/vite/src/views/products/plan/components/SaveChangesBar.tsx index 7012e4ab4..4aabb5081 100644 --- a/vite/src/views/products/plan/components/SaveChangesBar.tsx +++ b/vite/src/views/products/plan/components/SaveChangesBar.tsx @@ -46,7 +46,7 @@ export const SaveChangesBar = ({ const handleSaveClicked = async () => { if (!isOnboarding && isLoading) { - toast.error("Product counts are loading"); + toast.error("Plan counts are loading"); return; } diff --git a/vite/src/views/products/plan/components/SelectFeatureSheet.tsx b/vite/src/views/products/plan/components/SelectFeatureSheet.tsx index 5f82e3229..a30f72e68 100644 --- a/vite/src/views/products/plan/components/SelectFeatureSheet.tsx +++ b/vite/src/views/products/plan/components/SelectFeatureSheet.tsx @@ -62,7 +62,7 @@ export function SelectFeatureSheet({ {!isOnboarding && ( )} diff --git a/vite/src/views/products/plan/components/edit-plan-details/AdditionalOptions.tsx b/vite/src/views/products/plan/components/edit-plan-details/AdditionalOptions.tsx index acd74fcbb..800d77d87 100644 --- a/vite/src/views/products/plan/components/edit-plan-details/AdditionalOptions.tsx +++ b/vite/src/views/products/plan/components/edit-plan-details/AdditionalOptions.tsx @@ -14,7 +14,7 @@ export const AdditionalOptions = ({
diff --git a/vite/src/views/products/plan/components/edit-plan-details/BasePriceSection.tsx b/vite/src/views/products/plan/components/edit-plan-details/BasePriceSection.tsx index 2a867f84a..fd304d6ad 100644 --- a/vite/src/views/products/plan/components/edit-plan-details/BasePriceSection.tsx +++ b/vite/src/views/products/plan/components/edit-plan-details/BasePriceSection.tsx @@ -113,10 +113,9 @@ export const BasePriceSection = ({ }} description={ - Fixed recurring price (e.g., $100/month). Uncheck this section for{" "} - free or{" "} - usage-based only{" "} - plans. + A fixed price to charge for the plan. Uncheck this section if the plan + is free or{" "} + a variable price. } > diff --git a/vite/src/views/products/plan/components/edit-plan-details/MainDetailsSection.tsx b/vite/src/views/products/plan/components/edit-plan-details/MainDetailsSection.tsx index bc36f02ee..c7703ec12 100644 --- a/vite/src/views/products/plan/components/edit-plan-details/MainDetailsSection.tsx +++ b/vite/src/views/products/plan/components/edit-plan-details/MainDetailsSection.tsx @@ -8,7 +8,7 @@ export const MainDetailsSection = () => { const setProduct = useProductStore((s) => s.setProduct); return ( - +
@@ -31,7 +31,7 @@ export const MainDetailsSection = () => { {/*
Description
setProduct({ ...product, description: e.target.value }) diff --git a/vite/src/views/products/plan/components/edit-plan-feature/AdvancedSettings.tsx b/vite/src/views/products/plan/components/edit-plan-feature/AdvancedSettings.tsx index d93ab7a01..be4d037a9 100644 --- a/vite/src/views/products/plan/components/edit-plan-feature/AdvancedSettings.tsx +++ b/vite/src/views/products/plan/components/edit-plan-feature/AdvancedSettings.tsx @@ -81,10 +81,10 @@ export function AdvancedSettings() { description="Additional configuration options for this feature" >
- {/* Reset existing usage when product is enabled */} + {/* Reset existing usage when plan is enabled */}
diff --git a/vite/src/views/products/plan/components/plan-card/AddFeatureRow.tsx b/vite/src/views/products/plan/components/plan-card/AddFeatureRow.tsx index d7311f45e..3a06cbb73 100644 --- a/vite/src/views/products/plan/components/plan-card/AddFeatureRow.tsx +++ b/vite/src/views/products/plan/components/plan-card/AddFeatureRow.tsx @@ -24,13 +24,14 @@ export const AddFeatureRow = ({ disabled }: AddFeatureRowProps) => { return ( ); }; diff --git a/vite/src/views/products/plan/components/plan-card/PlanCardHeader.tsx b/vite/src/views/products/plan/components/plan-card/PlanCardHeader.tsx index 9c51e3056..fe10b3796 100644 --- a/vite/src/views/products/plan/components/plan-card/PlanCardHeader.tsx +++ b/vite/src/views/products/plan/components/plan-card/PlanCardHeader.tsx @@ -9,6 +9,8 @@ import { useIsEditingPlan, useSheetStore } from "@/hooks/stores/useSheetStore"; import { keyToTitle } from "@/utils/formatUtils/formatTextUtils"; import { PlanCardToolbar } from "./PlanCardToolbar"; +const MAX_PLAN_NAME_LENGTH = 20; + export const PlanCardHeader = () => { const navigate = useNavigate(); const product = useProductStore((s) => s.product); @@ -22,9 +24,14 @@ export const PlanCardHeader = () => {
- {product.name} + {product.name.length > MAX_PLAN_NAME_LENGTH + ? `${product.name.slice(0, MAX_PLAN_NAME_LENGTH)}...` + : product.name} - + MAX_PLAN_NAME_LENGTH - 10} + />
{ @@ -46,8 +53,8 @@ export const PlanCardHeader = () => { onClick={() => { setSheet({ type: "edit-plan", itemId: product.id }); }} - disabled={isPlanBeingEdited} - className="mt-2 !opacity-100" + disabled={true} + className="mt-2 !opacity-100 pointer-events-none" > {productV3.price?.amount ? ( diff --git a/vite/src/views/products/plan/components/plan-card/PlanCardToolbar.tsx b/vite/src/views/products/plan/components/plan-card/PlanCardToolbar.tsx index 88b984c7d..afd06e3e7 100644 --- a/vite/src/views/products/plan/components/plan-card/PlanCardToolbar.tsx +++ b/vite/src/views/products/plan/components/plan-card/PlanCardToolbar.tsx @@ -60,7 +60,7 @@ export const PlanCardToolbar = ({ } onClick={onEdit} - aria-label="Edit product" + aria-label="Edit plan" variant="muted" disabled={editDisabled} iconOrientation="center" @@ -75,7 +75,7 @@ export const PlanCardToolbar = ({ } onClick={() => setDeleteOpen(true)} - aria-label="Delete product" + aria-label="Delete plan" variant="muted" iconOrientation="center" disabled={deleteDisabled} diff --git a/vite/src/views/products/plan/hooks/useProductChangedAlert.tsx b/vite/src/views/products/plan/hooks/useProductChangedAlert.tsx index fd4e5eda1..89da89a8f 100644 --- a/vite/src/views/products/plan/hooks/useProductChangedAlert.tsx +++ b/vite/src/views/products/plan/hooks/useProductChangedAlert.tsx @@ -2,7 +2,7 @@ import { useCallback } from "react"; import { useBlocker } from "@/views/products/product/hooks/useBlocker"; const DEFAULT_MESSAGE = - "Are you sure you want to leave without updating the product? Your changes will be lost."; + "Are you sure you want to leave without updating the plan? Your changes will be lost."; export const useProductChangedAlert = ({ hasChanges, diff --git a/vite/src/views/products/product/ManageProduct.tsx b/vite/src/views/products/product/ManageProduct.tsx index 1eda62366..135056a8f 100644 --- a/vite/src/views/products/product/ManageProduct.tsx +++ b/vite/src/views/products/product/ManageProduct.tsx @@ -9,8 +9,8 @@ export const ManageProduct = ({ }: { hideAdminHover?: boolean; }) => { - const { customer } = useCusQuery(); - const { product, entityId } = useProductContext(); + const { product, entityId, isCusProductView } = useProductContext(); + const { customer } = useCusQuery({ enabled: isCusProductView }); return (
@@ -21,7 +21,7 @@ export const ManageProduct = ({ texts={[ { key: "internal_product_id", - value: product.internal_id!, + value: product.internal_id, }, { key: "stripe_id", diff --git a/vite/src/views/products/product/ProductSidebar.tsx b/vite/src/views/products/product/ProductSidebar.tsx index 1df61398c..4a359207c 100644 --- a/vite/src/views/products/product/ProductSidebar.tsx +++ b/vite/src/views/products/product/ProductSidebar.tsx @@ -91,7 +91,7 @@ export default function ProductSidebar() { } disabledReason={ isOneOffProduct(product.items, product.is_add_on) - ? "Can't add a free trial to an a one time product" + ? "Can't add a free trial to an a one time plan" : undefined } > @@ -100,7 +100,7 @@ export default function ProductSidebar() { ) : ( - Add a free trial to this product. + Add a free trial to this plan. )}
diff --git a/vite/src/views/products/product/ProductView.tsx b/vite/src/views/products/product/ProductView.tsx index 52379ac97..373eb7978 100644 --- a/vite/src/views/products/product/ProductView.tsx +++ b/vite/src/views/products/product/ProductView.tsx @@ -35,74 +35,20 @@ function ProductView() { if (error) { return ( - {error ? error.message : `Product ${product_id} not found`} + {error ? error.message : `Plan ${product_id} not found`} ); } if (!product) return; - // const updateProduct = async () => { - // try { - // await ProductService.updateProduct(axiosInstance, product.id, { - // ...UpdateProductSchema.parse(product), - // items: product.items, - // free_trial: product.free_trial, - // }); - - // if (isNewProduct) { - // toast.success("Product created successfully"); - // } else { - // toast.success("Product updated successfully"); - // } - - // await refetch(); - // await mutateCount(); - // } catch (error) { - // toast.error(getBackendErr(error, "Failed to update product")); - // } - // }; - - // const updateProductClicked = async () => { - // if (!counts) { - // toast.error("Something went wrong, please try again..."); - // return; - // } - - // if (version && version < data?.numVersions) { - // toast.error("You can only update the latest version of a product"); - // return; - // } - - // if (counts?.all > 0) { - // setShowNewVersionDialog(true); - // return; - // } - - // await updateProduct(); - // }; - return ( navigateTo("/products", navigate, env)} className="cursor-pointer" > - Products + Plans diff --git a/vite/src/views/products/product/components/UpdateProductButton.tsx b/vite/src/views/products/product/components/UpdateProductButton.tsx index 683a2c8c8..7e8842dba 100644 --- a/vite/src/views/products/product/components/UpdateProductButton.tsx +++ b/vite/src/views/products/product/components/UpdateProductButton.tsx @@ -18,7 +18,7 @@ export const UpdateProductButton = () => { const { refetch } = useProductQuery(); const handleUpdateClicked = async () => { - if (isLoading) toast.error("Product counts are loading"); + if (isLoading) toast.error("Plan counts are loading"); if (counts?.all > 0) { setShowNewVersionDialog(true); diff --git a/vite/src/views/products/product/hooks/NavigationBlockerModal.tsx b/vite/src/views/products/product/hooks/NavigationBlockerModal.tsx index e880fe123..ad7e395fe 100644 --- a/vite/src/views/products/product/hooks/NavigationBlockerModal.tsx +++ b/vite/src/views/products/product/hooks/NavigationBlockerModal.tsx @@ -26,7 +26,7 @@ export const NavigationBlockerModal: React.FC = ({ Unsaved Changes - Are you sure you want to leave without updating the product? Your + Are you sure you want to leave without updating the plan? Your changes will be lost. diff --git a/vite/src/views/products/product/hooks/queries/useMigrationsQuery.tsx.tsx b/vite/src/views/products/product/hooks/queries/useMigrationsQuery.tsx.tsx index 21bda6fce..9afcd5adb 100644 --- a/vite/src/views/products/product/hooks/queries/useMigrationsQuery.tsx.tsx +++ b/vite/src/views/products/product/hooks/queries/useMigrationsQuery.tsx.tsx @@ -1,5 +1,5 @@ -import { useAxiosInstance } from "@/services/useAxiosInstance"; import { useQuery } from "@tanstack/react-query"; +import { useAxiosInstance } from "@/services/useAxiosInstance"; export const useMigrationsQuery = () => { const axiosInstance = useAxiosInstance(); @@ -12,6 +12,7 @@ export const useMigrationsQuery = () => { const { data, isLoading, error, refetch } = useQuery({ queryKey: ["migrations"], queryFn: fetchProductMigrations, + retry: false, // Don't retry on error }); return { migrations: data?.migrations || [], isLoading, error, refetch }; diff --git a/vite/src/views/products/product/hooks/queries/useProductCountsQuery.tsx b/vite/src/views/products/product/hooks/queries/useProductCountsQuery.tsx index bc8c131c8..a1f7059a2 100644 --- a/vite/src/views/products/product/hooks/queries/useProductCountsQuery.tsx +++ b/vite/src/views/products/product/hooks/queries/useProductCountsQuery.tsx @@ -23,6 +23,8 @@ export const useProductCountsQuery = () => { const { data, isLoading, error, refetch } = useQuery({ queryKey: ["product_counts", productId, queryStates.version], queryFn: fetchProductCounts, + retry: false, // Don't retry on error (e.g., product not found) + enabled: !!productId, // Only run query if productId exists }); return { counts: data, isLoading, error, refetch }; diff --git a/vite/src/views/products/product/hooks/useProductData.tsx b/vite/src/views/products/product/hooks/useProductData.tsx index 58a936713..01125c571 100644 --- a/vite/src/views/products/product/hooks/useProductData.tsx +++ b/vite/src/views/products/product/hooks/useProductData.tsx @@ -79,10 +79,10 @@ export const useProductData = ({ const actionState = { disabled: !hasChanges, - buttonText: isNewProduct ? "Create Product" : "Update Product", + buttonText: isNewProduct ? "Create Plan" : "Update Plan", tooltipText: !hasChanges ? isNewProduct - ? "Add entitlements and prices to create a new product" + ? "Add entitlements and prices to create a new plan" : `Make a change to the entitlements or prices to update ${product?.name}` : isNewProduct ? `Create a new product: ${product?.name} ` diff --git a/vite/src/views/products/product/hooks/useProductQuery.tsx b/vite/src/views/products/product/hooks/useProductQuery.tsx index b7a7cf4b4..ec23cf600 100644 --- a/vite/src/views/products/product/hooks/useProductQuery.tsx +++ b/vite/src/views/products/product/hooks/useProductQuery.tsx @@ -1,8 +1,12 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; + import { parseAsInteger, parseAsString, useQueryStates } from "nuqs"; import { useMemo } from "react"; import { useParams } from "react-router"; import { useAxiosInstance } from "@/services/useAxiosInstance"; + +import { throwBackendError } from "@/utils/genUtils"; + import { useCachedProduct } from "./getCachedProduct"; import { useMigrationsQuery } from "./queries/useMigrationsQuery.tsx"; import { useProductCountsQuery } from "./queries/useProductCountsQuery"; @@ -44,15 +48,24 @@ export const useProductQuery = () => { queryParams.version = queryStates.version; } - const { data } = await axiosInstance.get(url, { params: queryParams }); - return data; + try { + const url = `/products/${productId}/data`; + const queryParams = { + version: queryStates.version, + }; + + const { data } = await axiosInstance.get(url, { params: queryParams }); + return data; + } catch (error) { + throwBackendError(error); + } }; const { data, isLoading, refetch, error } = useQuery({ queryKey: ["product", productId, queryStates.version], queryFn: fetcher, - retry: 1, // Fail faster - only retry once instead of default 3 times - retryDelay: 500, // Short delay between retries + retry: false, // Don't retry on error (e.g., product not found) + enabled: !!productId, // Only run query if productId exists }); const { refetch: refetchCounts } = useProductCountsQuery(); diff --git a/vite/src/views/products/product/prices/CreateFixedPrice.tsx b/vite/src/views/products/product/prices/CreateFixedPrice.tsx index 8e0b8e079..33581f133 100644 --- a/vite/src/views/products/product/prices/CreateFixedPrice.tsx +++ b/vite/src/views/products/product/prices/CreateFixedPrice.tsx @@ -90,7 +90,7 @@ function CreateFixedPrice() { try { if (hasChanges) { if (counts?.all > 0) { - toast.error("Please save the current changes to your product first"); + toast.error("Please save the current changes to your plan first"); return; } @@ -111,7 +111,7 @@ function CreateFixedPrice() { await navigate(getRedirectUrl(`/products/${newId}`, env)); } catch (error) { - toast.error(getBackendErr(error, "Failed to update product")); + toast.error(getBackendErr(error, "Failed to update plan")); } finally { setCopyLoading(false); } diff --git a/vite/src/views/products/product/product-item/product-item-config/PriceItemConfig.tsx b/vite/src/views/products/product/product-item/product-item-config/PriceItemConfig.tsx index 731127028..dab5d948c 100644 --- a/vite/src/views/products/product/product-item/product-item-config/PriceItemConfig.tsx +++ b/vite/src/views/products/product/product-item/product-item-config/PriceItemConfig.tsx @@ -21,7 +21,7 @@ export const PriceItemConfig = () => {
} isSelected={item.isVariable === false} onClick={() => { diff --git a/vite/src/views/products/product/product-sidebar/ToggleDefaultProduct.tsx b/vite/src/views/products/product/product-sidebar/ToggleDefaultProduct.tsx index 26e4922d7..02699b4a4 100644 --- a/vite/src/views/products/product/product-sidebar/ToggleDefaultProduct.tsx +++ b/vite/src/views/products/product/product-sidebar/ToggleDefaultProduct.tsx @@ -41,7 +41,7 @@ const ToggleProductDialog = ({ await toggleProduct(value, false); setOpen(false); } catch (error) { - toast.error(getBackendErr(error, "Failed to update product")); + toast.error(getBackendErr(error, "Failed to update plan")); } setLoading(false); }; @@ -49,7 +49,7 @@ const ToggleProductDialog = ({ const getTitle = () => { if (toggleKey === "is_default") { return value - ? `Make ${product.name} a default product` + ? `Make ${product.name} a default plan` : `Remove default from ${product.name}`; } else { return value @@ -119,14 +119,14 @@ export const ToggleDefaultProduct = ({ await ProductService.updateProduct(axiosInstance, product.id, data); // mutate(); setOpen(false); - toast.success("Successfully updated product"); + toast.success("Successfully updated plan"); } catch (error) { setProduct({ ...product, [toggleKey]: !value, }); - toast.error(getBackendErr(error, "Failed to update product")); + toast.error(getBackendErr(error, "Failed to update plan")); } finally { setToggling(false); } @@ -148,21 +148,21 @@ export const ToggleDefaultProduct = ({ if (toggleKey === "is_default") { if (value) { setDialogDescription( - `You have ${activeCount} active ${activeCusStr} on this product. Are you sure you want to make this product default?`, + `You have ${activeCount} active ${activeCusStr} on this plan. Are you sure you want to make this plan default?`, ); } else { setDialogDescription( - `You have ${activeCount} active ${activeCusStr} on this product. Are you sure you want to remove this product as default?`, + `You have ${activeCount} active ${activeCusStr} on this plan. Are you sure you want to remove this plan as default?`, ); } } else { if (value) { setDialogDescription( - `You have ${activeCount} active ${activeCusStr} on this product. Are you sure you want to make this product an add-on?`, + `You have ${activeCount} active ${activeCusStr} on this plan. Are you sure you want to make this plan an add-on?`, ); } else { setDialogDescription( - `You have ${activeCount} active ${activeCusStr} on this product. Are you sure you want to remove this product as an add-on?`, + `You have ${activeCount} active ${activeCusStr} on this plan. Are you sure you want to remove this plan as an add-on?`, ); } } @@ -180,11 +180,11 @@ export const ToggleDefaultProduct = ({ value && product.free_trial && !isFreeProductV2(product); if (isDefaultTrial && notNullish(groupDefaults?.defaultTrial)) { - return `${groupDefaults.defaultTrial.name} is currently a default trial product. Making ${product.name} a default trial will remove ${groupDefaults.defaultTrial.name} as a default trial product.`; + return `${groupDefaults.defaultTrial.name} is currently a default trial plan. Making ${product.name} a default trial will remove ${groupDefaults.defaultTrial.name} as a default trial plan.`; } if (value && notNullish(groupDefaults?.free)) { - return `${groupDefaults.free.name} is currently a default product. Making ${product.name} a default product will remove ${groupDefaults.free.name} as a default product.`; + return `${groupDefaults.free.name} is currently a default plan. Making ${product.name} a default plan will remove ${groupDefaults.free.name} as a default plan.`; } }; diff --git a/vite/src/views/products/products/ProductsPage.tsx b/vite/src/views/products/products/ProductsPage.tsx index b24e2fc67..fd46d856f 100644 --- a/vite/src/views/products/products/ProductsPage.tsx +++ b/vite/src/views/products/products/ProductsPage.tsx @@ -48,7 +48,7 @@ export const ProductsPage = () => { return (
@@ -75,8 +75,8 @@ export const ProductsPage = () => { { type: "item", label: queryStates.showArchivedProducts - ? `Show active products` - : `Show archived products`, + ? `Show active plans` + : `Show archived plans`, onClick: () => setQueryStates({ ...queryStates, diff --git a/vite/src/views/products/products/components/CopyProductDialog.tsx b/vite/src/views/products/products/components/CopyProductDialog.tsx index dded463ff..74de85f6e 100644 --- a/vite/src/views/products/products/components/CopyProductDialog.tsx +++ b/vite/src/views/products/products/components/CopyProductDialog.tsx @@ -48,7 +48,7 @@ export const CopyProductDialog = ({ const handleCopy = async () => { // 1. If env is the same and id is same, throw error if (env === toEnv && id === product.id) { - toast.error("Product ID already exists"); + toast.error("Plan ID already exists"); return; } @@ -61,11 +61,11 @@ export const CopyProductDialog = ({ }); await refetch(); - toast.success("Successfully copied product"); + toast.success("Successfully copied plan"); setOpen(false); } catch (error: unknown) { console.log("Error copying product", error); - toast.error(getBackendErr(error as AxiosError, "Failed to copy product")); + toast.error(getBackendErr(error as AxiosError, "Failed to copy plan")); } finally { setLoading(false); } diff --git a/vite/src/views/products/products/components/CreatePlanDialog.tsx b/vite/src/views/products/products/components/CreatePlanDialog.tsx index 08be0cb28..a641a0a44 100644 --- a/vite/src/views/products/products/components/CreatePlanDialog.tsx +++ b/vite/src/views/products/products/components/CreatePlanDialog.tsx @@ -47,8 +47,8 @@ function CreatePlanDialog({ if (!/^[a-zA-Z0-9 _-]+$/.test(productName)) { toast.error( !productName - ? "Product name is required" - : "Product name can only contain alphanumeric characters, dashes (-), and underscores (_)", + ? "Plan name is required" + : "Plan name can only contain alphanumeric characters, dashes (-), and underscores (_)", ); return; } @@ -67,7 +67,7 @@ function CreatePlanDialog({ } setOpen(false); } catch (error) { - toast.error(getBackendErr(error, "Failed to create product")); + toast.error(getBackendErr(error, "Failed to create plan")); } setLoading(false); }; @@ -88,11 +88,11 @@ function CreatePlanDialog({ className={buttonClassName} onClick={() => setOpen(true)} > - Add Product + Add Plan - Create Product + Create Plan - Create Product + Create Plan diff --git a/vite/src/views/products/products/components/CreateProductDialog.tsx b/vite/src/views/products/products/components/CreateProductDialog.tsx index 6846e056d..2d36bdac6 100644 --- a/vite/src/views/products/products/components/CreateProductDialog.tsx +++ b/vite/src/views/products/products/components/CreateProductDialog.tsx @@ -46,8 +46,8 @@ function CreateProduct({ if (!/^[a-zA-Z0-9 _-]+$/.test(productName)) { toast.error( !productName - ? "Product name is required" - : "Product name can only contain alphanumeric characters, dashes (-), and underscores (_)", + ? "Plan name is required" + : "Plan name can only contain alphanumeric characters, dashes (-), and underscores (_)", ); return; } @@ -66,7 +66,7 @@ function CreateProduct({ } setOpen(false); } catch (error) { - toast.error(getBackendErr(error, "Failed to create product")); + toast.error(getBackendErr(error, "Failed to create plan")); } setLoading(false); }; @@ -83,11 +83,11 @@ function CreateProduct({ - Create Product + Create Plan setProduct({ @@ -119,7 +119,7 @@ function CreateProduct({ setProduct({ ...product, is_add_on: !product?.is_add_on }) @@ -132,7 +132,7 @@ function CreateProduct({ variant="gradientPrimary" className="min-w-44 w-44 max-w-44" > - Create Product + Create Plan
diff --git a/vite/src/views/products/products/components/CreateProductMainDetails.tsx b/vite/src/views/products/products/components/CreateProductMainDetails.tsx index 48d3d39ca..66409510e 100644 --- a/vite/src/views/products/products/components/CreateProductMainDetails.tsx +++ b/vite/src/views/products/products/components/CreateProductMainDetails.tsx @@ -16,13 +16,13 @@ export const CreateProductMainDetails = () => { }); return ( - +
Name setSource(e.target.value)} /> diff --git a/vite/src/views/products/products/components/CreateProductSheet.tsx b/vite/src/views/products/products/components/CreateProductSheet.tsx index 19a87c820..b4a2dd6d1 100644 --- a/vite/src/views/products/products/components/CreateProductSheet.tsx +++ b/vite/src/views/products/products/components/CreateProductSheet.tsx @@ -51,8 +51,8 @@ function CreateProductSheet({ if (!/^[a-zA-Z0-9 _-]+$/.test(productName)) { toast.error( !productName - ? "Product name is required" - : "Product name can only contain alphanumeric characters, dashes (-), and underscores (_)", + ? "Plan name is required" + : "Plan name can only contain alphanumeric characters, dashes (-), and underscores (_)", ); return; } @@ -72,7 +72,7 @@ function CreateProductSheet({ setOpen(false); } catch (error) { toast.error( - getBackendErr(error as AxiosError, "Failed to create product"), + getBackendErr(error as AxiosError, "Failed to create plan"), ); } setLoading(false); @@ -94,13 +94,13 @@ function CreateProductSheet({
@@ -124,7 +124,7 @@ function CreateProductSheet({ metaShortcut="enter" isLoading={loading} > - Create product + Create plan diff --git a/vite/src/views/products/products/components/UpdateProductDialog.tsx b/vite/src/views/products/products/components/UpdateProductDialog.tsx index a7720fdb8..b8d4c3b1a 100644 --- a/vite/src/views/products/products/components/UpdateProductDialog.tsx +++ b/vite/src/views/products/products/components/UpdateProductDialog.tsx @@ -45,12 +45,12 @@ export const UpdateProductDialog = ({ await refetch(); setOpen(false); - toast.success(`Successfully updated product ${product.id}`); + toast.success(`Successfully updated plan ${product.id}`); } catch (error: unknown) { toast.error( - getBackendErr(error as AxiosError, "Failed to update product"), + getBackendErr(error as AxiosError, "Failed to update plan"), ); - } finally { + } finally{ setLoading(false); } }; @@ -59,7 +59,7 @@ export const UpdateProductDialog = ({ e.stopPropagation()}> - Edit Product + Edit Plan
diff --git a/vite/src/views/products/rewards/reward-config/components/FreeProductRewardConfig.tsx b/vite/src/views/products/rewards/reward-config/components/FreeProductRewardConfig.tsx index 4b41a8c47..ad25711ac 100644 --- a/vite/src/views/products/rewards/reward-config/components/FreeProductRewardConfig.tsx +++ b/vite/src/views/products/rewards/reward-config/components/FreeProductRewardConfig.tsx @@ -30,10 +30,10 @@ export function FreeProductRewardConfig({ const isEmpty = freeAddOns.length === 0; return ( - + {/* Product Selection */}
- Product + Plan