diff --git a/bun.lock b/bun.lock index 43e9c7ab7..26aed55d7 100644 --- a/bun.lock +++ b/bun.lock @@ -275,8 +275,6 @@ "@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=="], @@ -333,7 +331,6 @@ "@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"], @@ -388,17 +385,11 @@ "@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=="], @@ -424,15 +415,6 @@ "@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=="], - - "@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=="], @@ -440,7 +422,6 @@ "@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=="], @@ -466,59 +447,6 @@ "@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=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.10", "", { "os": "android", "cpu": "arm64" }, "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.25.10", "", { "os": "android", "cpu": "x64" }, "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.10", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.10", "", { "os": "linux", "cpu": "arm" }, "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.10", "", { "os": "linux", "cpu": "ia32" }, "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.10", "", { "os": "linux", "cpu": "none" }, "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.10", "", { "os": "none", "cpu": "x64" }, "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.10", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.10", "", { "os": "openbsd", "cpu": "x64" }, "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.10", "", { "os": "none", "cpu": "arm64" }, "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.10", "", { "os": "sunos", "cpu": "x64" }, "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw=="], - - "@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=="], @@ -570,7 +498,6 @@ "@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=="], @@ -578,21 +505,13 @@ "@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.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], @@ -770,25 +689,6 @@ "@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=="], - - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-cacs/WQqa96IhqUm+7CY+z/0j9sW6X80KE07v3IAJuv+z0UNvJtKSlT/T1w1SpaQRa9l0wCYYZlRZUhUOvEVmg=="], - - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-tLd90SvkRFik6LSfuYjcJEmwqcNEnVYVOyKTacSazya/SLlSwy/VYKsDE4GIzOBd+h3gW+FXqShc2XBavccHCg=="], - - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ekV76G2R/l3nkvylkfy9jBSYHeB4QcJ7LdDseT6INnn1p51bmDS1eGoSoq+RxfQ7B1wt+Qa0pIl5aqcx0GLpbw=="], - - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.5", "", { "os": "linux", "cpu": "x64" }, "sha512-tI+sBu+3FmWtqlqD4xKJcj3KJtqbniLombKTE7/UWyyoHmOyAo3aZ7QcEHIOgInXOG1nt0rwh0KGmNbvSB0Djg=="], - - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.5", "", { "os": "linux", "cpu": "x64" }, "sha512-kDRh+epN/ulroNJLr+toDjN+/JClY5L+OAWjOrrKCI0qcKvTw9GBx7CU/rdA2bgi4WpZN3l0rf/3+b8rduEwrQ=="], - - "@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=="], @@ -806,7 +706,6 @@ "@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=="], @@ -1001,11 +900,8 @@ "@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=="], @@ -1161,55 +1057,6 @@ "@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=="], - - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.52.4", "", { "os": "android", "cpu": "arm" }, "sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA=="], - - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.52.4", "", { "os": "android", "cpu": "arm64" }, "sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w=="], - - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.52.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg=="], - - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.52.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw=="], - - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.52.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ=="], - - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.52.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw=="], - - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ=="], - - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.52.4", "", { "os": "linux", "cpu": "arm" }, "sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q=="], - - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg=="], - - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.52.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g=="], - - "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ=="], - - "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.52.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g=="], - - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg=="], - - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.52.4", "", { "os": "linux", "cpu": "none" }, "sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA=="], - - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.52.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA=="], - - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg=="], - - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.52.4", "", { "os": "linux", "cpu": "x64" }, "sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw=="], - - "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.52.4", "", { "os": "none", "cpu": "arm64" }, "sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA=="], - - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.52.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ=="], - - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.52.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw=="], - - "@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=="], @@ -1261,7 +1108,6 @@ "@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=="], @@ -1295,23 +1141,6 @@ "@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=="], - - "@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.0", "", { "dependencies": { "@supabase/node-fetch": "2.6.15" } }, "sha512-YfBz4W/z7eYCFyuvHhfjOTTzRrQIvsMG2bVwJAKEVVUqGdzqfvyidXssLBG0Fqlql1zJFgtsPpK1n4meHrI7tg=="], - - "@supabase/realtime-js": ["@supabase/realtime-js@2.75.0", "", { "dependencies": { "@supabase/node-fetch": "2.6.15", "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "ws": "^8.18.2" } }, "sha512-B4Xxsf2NHd5cEnM6MGswOSPSsZKljkYXpvzKKmNxoUmNQOfB7D8HOa6NwHcUBSlxcjV+vIrYKcYXtavGJqeGrw=="], - - "@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.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=="], @@ -1327,7 +1156,6 @@ "@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=="], @@ -1361,19 +1189,11 @@ "@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=="], @@ -1423,11 +1243,8 @@ "@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=="], @@ -1486,11 +1303,7 @@ "@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=="], @@ -1522,7 +1335,7 @@ "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], - "@types/send": ["@types/send@1.2.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ=="], + "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], "@types/serve-static": ["@types/serve-static@1.15.9", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA=="], @@ -1566,23 +1379,6 @@ "@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=="], - - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251014.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-SF29o9NFRGDM23Jz0nVO4/yS78GQ81rtOemmCVNXuJotoY4bP3npGDyEmfkZQHZgDOXogs2OWy3t7NUJ235ANQ=="], - - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251014.1", "", { "os": "linux", "cpu": "arm" }, "sha512-o5cu7h+BBAp6V4qxYY5RWuaYouN3j+MGFLrrUtvvNj4XKM+kbq5qwsgVRsmJZ1LfUvHmzyQs86vt9djAWedzjQ=="], - - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251014.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-+YWbW/JF4uggEUBr+vflqI5i7bL4Z3XInCOyUO1qQEY7VmfDCsPEzIwGi37O1mixfxw9Qj8LQsptCkU+fqKwGw=="], - - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251014.1", "", { "os": "linux", "cpu": "x64" }, "sha512-3LC4tgcgi6zWJWBUpBNXOGSY3yISJrQezSP/T+v+mQRApkdoIpTSHIyQAhgaagcs3MOQRaqiIPaLOVrdHXdU6A=="], - - "@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=="], @@ -1598,7 +1394,6 @@ "@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=="], @@ -1686,11 +1481,7 @@ "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=="], @@ -1708,15 +1499,6 @@ "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=="], - - "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=="], @@ -1726,7 +1508,6 @@ "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=="], @@ -1770,11 +1551,7 @@ "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=="], @@ -1854,11 +1631,7 @@ "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=="], @@ -1936,11 +1709,8 @@ "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=="], @@ -2011,11 +1781,7 @@ "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=="], @@ -2043,11 +1809,7 @@ "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=="], @@ -2059,19 +1821,11 @@ "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=="], @@ -2101,11 +1855,8 @@ "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=="], @@ -2254,11 +2005,7 @@ "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=="], @@ -2546,11 +2293,8 @@ "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=="], @@ -2574,11 +2318,8 @@ "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=="], @@ -2586,21 +2327,14 @@ "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=="], @@ -2609,11 +2343,7 @@ "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=="], @@ -2621,11 +2351,7 @@ "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=="], @@ -2705,11 +2431,7 @@ "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=="], @@ -2731,20 +2453,13 @@ "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=="], @@ -2780,11 +2495,7 @@ "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=="], @@ -2856,11 +2567,7 @@ "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=="], @@ -2894,11 +2601,7 @@ "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=="], @@ -2956,19 +2659,14 @@ "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=="], - "slow-redact": ["slow-redact@0.3.2", "", {}, "sha512-MseHyi2+E/hBRqdOi5COy6wZ7j7DxXRz9NkseavNYSvvWC06D8a5cidVZX3tcG5eCW3NIyVU4zT63hw0Q486jw=="], - "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], "socket.io": ["socket.io@4.8.1", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg=="], @@ -3027,11 +2725,7 @@ "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=="], @@ -3112,11 +2806,8 @@ "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=="], @@ -3180,11 +2871,7 @@ "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 + "vite": ["vite@6.4.1", "", { "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-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], "vscode-oniguruma": ["vscode-oniguruma@2.0.1", "", {}, "sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ=="], @@ -3256,13 +2943,6 @@ "@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=="], @@ -3288,7 +2968,6 @@ "@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=="], @@ -3308,11 +2987,8 @@ "@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=="], @@ -3624,11 +3300,6 @@ "@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=="], @@ -3694,11 +3365,6 @@ "@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=="], @@ -3803,11 +3469,7 @@ "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=="], @@ -4246,13 +3908,10 @@ "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=="], @@ -4360,11 +4019,8 @@ "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/server/customer.json b/server/customer.json new file mode 100644 index 000000000..5e1a50cc5 --- /dev/null +++ b/server/customer.json @@ -0,0 +1,277 @@ +{ + "id": "123", + "created_at": 1761032316983, + "name": "123", + "email": "123@example.com", + "fingerprint": null, + "stripe_id": "cus_TH8QGlLFtlDpAj", + "env": "sandbox", + "metadata": {}, + "products": [ + { + "id": "pro", + "name": "Pro", + "group": null, + "status": "trialing", + "canceled_at": null, + "started_at": 1761122358550, + "is_default": false, + "is_add_on": false, + "version": 5, + "current_period_start": 1761032316000, + "current_period_end": 1762073321000, + "items": [ + { + "type": "price", + "feature_id": null, + "feature": null, + "interval": "month", + "interval_count": 1, + "price": 20 + }, + { + "type": "priced_feature", + "feature_id": "messages", + "feature_type": "single_use", + "feature": { + "id": "messages", + "name": "messages", + "type": "single_use", + "display": { + "singular": "message", + "plural": "messages" + }, + "archived": false + }, + "included_usage": 200, + "interval": "month", + "interval_count": 1, + "price": 0.5, + "usage_model": "pay_per_use", + "billing_units": 1, + "reset_usage_when_enabled": true, + "entity_feature_id": null + }, + { + "type": "priced_feature", + "feature_id": "websites", + "feature_type": "continuous_use", + "feature": { + "id": "websites", + "name": "Websites", + "type": "continuous_use", + "display": { + "singular": "website", + "plural": "websites" + }, + "archived": false + }, + "included_usage": 3, + "interval": "month", + "interval_count": 1, + "price": 20, + "usage_model": "pay_per_use", + "billing_units": 1, + "reset_usage_when_enabled": false, + "entity_feature_id": null + }, + { + "type": "feature", + "feature_id": "discord_notifications", + "feature_type": "static", + "feature": { + "id": "discord_notifications", + "name": "Discord notifications", + "type": "boolean", + "display": { + "singular": "Discord notification", + "plural": "Discord notifications" + }, + "archived": false + }, + "entity_feature_id": null + }, + { + "type": "feature", + "feature_id": "messages", + "feature_type": "single_use", + "feature": { + "id": "messages", + "name": "messages", + "type": "single_use", + "display": { + "singular": "message", + "plural": "messages" + }, + "archived": false + }, + "included_usage": 500, + "interval": null, + "interval_count": 1, + "reset_usage_when_enabled": true, + "entity_feature_id": null + }, + { + "type": "feature", + "feature_id": "projects", + "feature_type": "continuous_use", + "feature": { + "id": "projects", + "name": "Projects", + "type": "continuous_use", + "display": { + "singular": "project", + "plural": "projects" + }, + "archived": false + }, + "included_usage": 12, + "interval": null, + "interval_count": 1, + "reset_usage_when_enabled": false, + "entity_feature_id": null + }, + { + "type": "feature", + "feature_id": "swag", + "feature_type": "single_use", + "feature": { + "id": "swag", + "name": "Swag", + "type": "single_use", + "display": { + "singular": "swag", + "plural": "swags" + }, + "archived": false + }, + "included_usage": "inf", + "interval": null, + "interval_count": 1, + "reset_usage_when_enabled": true, + "entity_feature_id": null + } + ], + "quantity": 1 + } + ], + "features": { + "discord_notifications": { + "id": "discord_notifications", + "type": "static", + "name": "Discord notifications", + "unlimited": false, + "balance": 0, + "usage": 0, + "included_usage": 0, + "next_reset_at": null, + "overage_allowed": false + }, + "swag": { + "id": "swag", + "type": "single_use", + "name": "Swag", + "unlimited": true, + "balance": 0, + "usage": 0, + "included_usage": 0, + "next_reset_at": null, + "overage_allowed": false + }, + "messages": { + "id": "messages", + "type": "single_use", + "name": "messages", + "interval": "multiple", + "interval_count": null, + "unlimited": false, + "balance": 550, + "usage": 150, + "included_usage": 700, + "next_reset_at": 1764665321000, + "overage_allowed": false, + "breakdown": [ + { + "interval": "lifetime", + "interval_count": 1, + "balance": 350, + "usage": 150, + "included_usage": 500, + "next_reset_at": null + }, + { + "interval": "month", + "interval_count": 1, + "balance": 200, + "usage": 0, + "included_usage": 200, + "next_reset_at": 1764665321000, + "usage_limit": 500 + } + ], + "usage_limit": 1000 + }, + "projects": { + "id": "projects", + "type": "continuous_use", + "name": "Projects", + "interval": "lifetime", + "interval_count": 1, + "unlimited": false, + "balance": 12, + "usage": 0, + "included_usage": 12, + "next_reset_at": null, + "overage_allowed": false + }, + "websites": { + "id": "websites", + "type": "continuous_use", + "name": "Websites", + "interval": "lifetime", + "interval_count": 1, + "unlimited": false, + "balance": 3, + "usage": 0, + "included_usage": 3, + "next_reset_at": null, + "overage_allowed": false, + "usage_limit": 10 + } + }, + "invoices": [ + { + "product_ids": [ + "pro" + ], + "stripe_id": "in_1SKi259mx3u0jkgOr86nPA3R", + "status": "paid", + "total": 0, + "currency": "usd", + "created_at": 1761467921000, + "hosted_invoice_url": "http://localhost:8080/invoices/hosted_invoice_url/inv_34Nmt7zkME1IpjKXCBj0Mr6EBOx" + }, + { + "product_ids": [ + "pro" + ], + "stripe_id": "in_1SKi1F9mx3u0jkgO38q4Vf2B", + "status": "paid", + "total": 0, + "currency": "usd", + "created_at": 1761467921000, + "hosted_invoice_url": "http://localhost:8080/invoices/hosted_invoice_url/inv_34Nmmg52fFTqhf1rJhzKgCjEvzj" + }, + { + "product_ids": [ + "pro" + ], + "stripe_id": "in_1SKa9U9mx3u0jkgOpZa0MZFx", + "status": "paid", + "total": 0, + "currency": "usd", + "created_at": 1761032316000, + "hosted_invoice_url": "http://localhost:8080/invoices/hosted_invoice_url/inv_34MnUDUGNVi0hdc21Rg3gb8h24d" + } + ] +} \ No newline at end of file diff --git a/server/example.ts b/server/example.ts index 1f4ddbb12..d06f117b4 100644 --- a/server/example.ts +++ b/server/example.ts @@ -1,14 +1,8 @@ // @ts-nocheck -{ - id: "team_plan", - // other fields, - licenses: [ - { plan_id: "dev_seat" }, - { - plan_id: "pro_seat", price: 50 // to override price - }, - ] -} + + +// Old customer object + // Global namesapce: // diff --git a/server/src/errors/formatZodError.ts b/server/src/errors/formatZodError.ts new file mode 100644 index 000000000..d6775c9b8 --- /dev/null +++ b/server/src/errors/formatZodError.ts @@ -0,0 +1,59 @@ +import type { ZodError, ZodIssue } from "zod/v4"; + +/** + * Formats Zod validation errors into user-friendly messages + */ +export function formatZodError(error: ZodError): string { + const formatMessage = (issue: ZodIssue): string => { + const path = issue.path.length ? issue.path.join(".") : "input"; + + // Clean up common Zod error messages + let message = issue.message; + + // Handle common patterns and make them more user-friendly + if ( + message.includes("Too small") && + message.includes("expected string to have >=1 characters") + ) { + message = "cannot be empty"; + } else if (message.includes("Invalid option: expected one of")) { + // Clean up enum error messages + // Example: 'Invalid option: expected one of "a"|"b"|"c"' -> 'must be one of: a, b, c' + const match = message.match(/expected one of (.+)/); + if (match) { + const options = match[1] + .split("|") + .map((opt) => opt.replace(/['"]/g, "").trim()) + .join(", "); + message = `must be one of: ${options}`; + } + } else if (message.includes("Invalid string: must match pattern")) { + // Extract the pattern and make it more readable + if (message.includes("/^[a-zA-Z0-9_-]+$/")) { + message = + "must contain only letters, numbers, underscores, and hyphens"; + } else { + message = "has invalid format"; + } + } else if (message.includes("Invalid input: expected string, received")) { + const receivedType = message.split("received ")[1]; + message = `must be a string (received ${receivedType})`; + } else if (message.includes("Invalid input: expected number, received")) { + const receivedType = message.split("received ")[1]; + message = `must be a number (received ${receivedType})`; + } else if (message.includes("Invalid input: expected boolean, received")) { + const receivedType = message.split("received ")[1]; + message = `must be a boolean (received ${receivedType})`; + } + + return `${path}: ${message}`; + }; + + const formattedIssues = error.issues.map(formatMessage); + + // If there are multiple issues, format them nicely + if (formattedIssues.length === 1) { + return formattedIssues[0]; + } + return `[Validation Errors] ${formattedIssues.join("; ")}`; +} diff --git a/server/src/external/stripe/priceToStripeItem/priceToStripeItem.ts b/server/src/external/stripe/priceToStripeItem/priceToStripeItem.ts index 2d53f2bd6..14f2c03c1 100644 --- a/server/src/external/stripe/priceToStripeItem/priceToStripeItem.ts +++ b/server/src/external/stripe/priceToStripeItem/priceToStripeItem.ts @@ -140,7 +140,7 @@ export const priceToStripeItem = ({ }; } - if (apiVersion === ApiVersion.Beta && !isCheckout) { + if (apiVersion === ApiVersion.V1_Beta && !isCheckout) { return { lineItem: { // lineItem: getEmptyPriceItem({ price, org }), diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts index 2609c78d3..b9f7b46db 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleCheckoutSub.ts @@ -75,7 +75,7 @@ export const handleCheckoutSub = async ({ if ( arrearPrice && (attachParams.internalEntityId || - attachParams.apiVersion === ApiVersion.Beta) + attachParams.apiVersion === ApiVersion.V1_Beta) ) { itemsUpdate.push({ id: item.id, diff --git a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts index 695120f37..01afe9ef8 100644 --- a/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts +++ b/server/src/external/stripe/webhookHandlers/handleCheckoutCompleted/handleRemainingSets.ts @@ -38,7 +38,7 @@ export const handleRemainingSets = async ({ if ( attachParams.internalEntityId || - attachParams.apiVersion === ApiVersion.Beta + attachParams.apiVersion === ApiVersion.V1_Beta ) { const replaceIndex = remainingItems.findIndex( (item) => item.price === config.stripe_price_id, diff --git a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts index ab27e56dc..1a008b8d2 100644 --- a/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts +++ b/server/src/external/stripe/webhookHandlers/handleInvoiceCreated/handleUsagePrices.ts @@ -17,11 +17,11 @@ import { RolloverService } from "@/internal/customers/cusProducts/cusEnts/cusRol import { getRolloverUpdates } from "@/internal/customers/cusProducts/cusEnts/cusRollovers/rolloverUtils.js"; import { getResetBalancesUpdate } from "@/internal/customers/cusProducts/cusEnts/groupByUtils.js"; import { getCusPriceUsage } from "@/internal/customers/cusProducts/cusPrices/cusPriceUtils.js"; +import { getAllFullCustomers } from "@/utils/scriptUtils/getAll/getAllAutumnCustomers.js"; import { submitUsageToStripe } from "../../stripeMeterUtils.js"; import { getInvoiceItemForUsage } from "../../stripePriceUtils.js"; import { subToPeriodStartEnd } from "../../stripeSubUtils/convertSubUtils.js"; import { findStripeItemForPrice } from "../../stripeSubUtils/stripeSubItemUtils.js"; -import { getAllFullCustomers } from "@/utils/scriptUtils/getAll/getAllAutumnCustomers.js"; export const handleUsagePrices = async ({ db, @@ -86,7 +86,7 @@ export const handleUsagePrices = async ({ const isNewUsageMethod = activeProduct.internal_entity_id || - activeProduct.api_semver === ApiVersion.Beta; + activeProduct.api_semver === ApiVersion.V1_Beta; if (isNewUsageMethod) { const invoiceItem = getInvoiceItemForUsage({ diff --git a/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts b/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts index 828db6486..bc73c4caa 100644 --- a/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts +++ b/server/src/external/stripe/webhookHandlers/handleSubDeleted/handleCusProductDeleted.ts @@ -53,7 +53,7 @@ export const handleCusProductDeleted = async ({ stripeId: fullCus.processor?.id, }); - const isV4Usage = cusProduct.api_semver === ApiVersion.Beta; + const isV4Usage = cusProduct.api_semver === ApiVersion.V1_Beta; // refer to handleUpgradeFlow.ts, when cancel immediately through API / dashboard, this happens...? const isAutumnCancel = diff --git a/server/src/honoMiddlewares/apiVersionMiddleware.ts b/server/src/honoMiddlewares/apiVersionMiddleware.ts index fbe4a1fac..03f1547cf 100644 --- a/server/src/honoMiddlewares/apiVersionMiddleware.ts +++ b/server/src/honoMiddlewares/apiVersionMiddleware.ts @@ -47,6 +47,7 @@ export const apiVersionMiddleware = async (c: Context, next: Next) => { finalVersion = new ApiVersionClass(parsedVersion); } + // 2. Calculate from org creation date else if (org?.created_at) { finalVersion = createdAtToVersion({ diff --git a/server/src/honoMiddlewares/baseMiddleware.ts b/server/src/honoMiddlewares/baseMiddleware.ts index 2414f451d..52647cb96 100644 --- a/server/src/honoMiddlewares/baseMiddleware.ts +++ b/server/src/honoMiddlewares/baseMiddleware.ts @@ -68,6 +68,9 @@ export const baseMiddleware = async (c: Context, next: Next) => { userId: undefined, authType: AuthType.Unknown, env: AppEnv.Sandbox, // maybe use app_env headers + + // Query params + expand: [], }); childLogger.info(`${method} ${path}`); diff --git a/server/src/honoMiddlewares/errorMiddleware.ts b/server/src/honoMiddlewares/errorMiddleware.ts index c7d475647..4b23ad98d 100644 --- a/server/src/honoMiddlewares/errorMiddleware.ts +++ b/server/src/honoMiddlewares/errorMiddleware.ts @@ -3,8 +3,9 @@ import type { Context } from "hono"; import type { ContentfulStatusCode } from "hono/utils/http-status"; import Stripe from "stripe"; import { ZodError } from "zod/v4"; +import { formatZodError } from "@/errors/formatZodError.js"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; -import RecaseError, { formatZodError } from "@/utils/errorUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; import { handleErrorSkip } from "./errorSkipMiddleware.js"; /** diff --git a/server/src/honoMiddlewares/errorSkipMiddleware.ts b/server/src/honoMiddlewares/errorSkipMiddleware.ts index 84619431b..e77459556 100644 --- a/server/src/honoMiddlewares/errorSkipMiddleware.ts +++ b/server/src/honoMiddlewares/errorSkipMiddleware.ts @@ -8,8 +8,9 @@ import type { Context } from "hono"; import type { ContentfulStatusCode } from "hono/utils/http-status"; import Stripe from "stripe"; import { ZodError } from "zod/v4"; +import { formatZodError } from "@/errors/formatZodError.js"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; -import RecaseError, { formatZodError } from "@/utils/errorUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; import { matchRoute } from "./middlewareUtils.js"; // ============================================================================ diff --git a/server/src/honoMiddlewares/expandMiddleware.ts b/server/src/honoMiddlewares/expandMiddleware.ts new file mode 100644 index 000000000..d771f8d5e --- /dev/null +++ b/server/src/honoMiddlewares/expandMiddleware.ts @@ -0,0 +1,21 @@ +import type { MiddlewareHandler } from "hono"; +import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; + +/** + * Extracts expand from validated query and sets it in context. + * Must run AFTER versionedValidator so expand has been transformed. + */ +export const expandMiddleware = (): MiddlewareHandler => { + return async (c, next) => { + const validatedQuery = c.req.valid("query"); + const expand = validatedQuery?.expand || []; + + const ctx = c.get("ctx"); + c.set("ctx", { + ...ctx, + expand: Array.isArray(expand) ? expand : [expand], + }); + + await next(); + }; +}; diff --git a/server/src/honoMiddlewares/routeHandler.ts b/server/src/honoMiddlewares/routeHandler.ts index 377505cca..3a0a613d9 100644 --- a/server/src/honoMiddlewares/routeHandler.ts +++ b/server/src/honoMiddlewares/routeHandler.ts @@ -5,6 +5,7 @@ import type { DrizzleCli } from "@/db/initDrizzle.js"; import type { HonoEnv } from "@/honoUtils/HonoEnv.js"; import { validator } from "./validatorMiddleware.js"; import { versionedValidator } from "./versionedValidator.js"; +import { expandMiddleware } from "./expandMiddleware.js"; /** * Extended context type that includes validated input @@ -128,6 +129,11 @@ export function createRoute< middlewares.push(validator("param", opts.params)); } + // Add expand middleware after query validation + if (opts.query || opts.versionedQuery) { + middlewares.push(expandMiddleware()); + } + const wrappedHandler = async ( c: ValidatedContext, ) => { diff --git a/server/src/honoMiddlewares/versionedValidator.ts b/server/src/honoMiddlewares/versionedValidator.ts index 2a87ee920..88f32a411 100644 --- a/server/src/honoMiddlewares/versionedValidator.ts +++ b/server/src/honoMiddlewares/versionedValidator.ts @@ -55,28 +55,39 @@ export const versionedValidator = ({ const versionKey = userVersion.value as ApiVersion; const schema = schemas[versionKey] ?? schemas.latest; - // Validate with version-specific schema using zValidator - const validatorMiddleware = zValidator(target, schema, (result, _c) => { + let validatedData: Record; + + // For query validation, use the parsed query from queryMiddleware + if (target === "query") { + const parsedQuery = c.req.query(); + const result = schema.safeParse(parsedQuery); if (!result.success) { - // Validation errors reference fields from user's version ✅ throw result.error; } - }); + validatedData = result.data as Record; + c.req.addValidatedData(target, result.data); + } else { + // For other targets, use zValidator + const validatorMiddleware = zValidator(target, schema, (result, _c) => { + if (!result.success) { + // Validation errors reference fields from user's version ✅ + throw result.error; + } + }); - // Run validation - await validatorMiddleware(c, async () => {}); + // Run validation + await validatorMiddleware(c, async () => {}); - // Get validated data - type assertion needed due to Hono's dynamic validation target typing - type ValidData = Record; - // biome-ignore lint/suspicious/noExplicitAny: Hono's typing requires dynamic target access - const validatedData = (c.req as any).valid(target) as ValidData; + // Get validated data - type assertion needed due to Hono's dynamic validation target typing + validatedData = (c.req as any).valid(target) as Record; + } // If user is on older version, transform to latest if (!userVersion.eq(new ApiVersionClass(LATEST_VERSION))) { const transformed = applyRequestVersionChanges({ input: validatedData, - targetVersion: userVersion, - currentVersion: new ApiVersionClass(LATEST_VERSION), + fromVersion: userVersion, + toVersion: new ApiVersionClass(LATEST_VERSION), resource, }); diff --git a/server/src/honoUtils/HonoEnv.ts b/server/src/honoUtils/HonoEnv.ts index 2ffd54115..79193d472 100644 --- a/server/src/honoUtils/HonoEnv.ts +++ b/server/src/honoUtils/HonoEnv.ts @@ -19,7 +19,7 @@ export type RequestContext = { // Objects db: DrizzleCli; logger: Logger; - clickhouseClient: ClickHouseClient; + clickhouseClient?: ClickHouseClient; // Info id: string; @@ -27,6 +27,9 @@ export type RequestContext = { authType: AuthType; apiVersion: ApiVersionClass; timestamp: number; + + // Query params + expand: string[]; }; export type AutumnContext = RequestContext; diff --git a/server/src/internal/analytics/handlers/handleCustomerCreated.ts b/server/src/internal/analytics/handlers/handleCustomerCreated.ts index 184995814..0a951b0de 100644 --- a/server/src/internal/analytics/handlers/handleCustomerCreated.ts +++ b/server/src/internal/analytics/handlers/handleCustomerCreated.ts @@ -1,11 +1,12 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; -import { ActionType, AppEnv, Organization } from "@autumn/shared"; -import { addTaskToQueue } from "@/queue/queueUtils.js"; -import { JobName } from "@/queue/JobName.js"; -import { constructAction, parseReqForAction } from "../actionUtils.js"; +import { ActionType, type AppEnv, type Organization } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { CusService } from "@/internal/customers/CusService.js"; +import { JobName } from "@/queue/JobName.js"; +import { addTaskToQueue } from "@/queue/queueUtils.js"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; +import type { HandleCustomerCreatedData } from "@/utils/workerUtils/jobTypes/HandleCustomerCreatedData.js"; import { ActionService } from "../ActionService.js"; +import { constructAction, parseReqForAction } from "../actionUtils.js"; export const addCustomerCreatedTask = async ({ req, @@ -23,31 +24,30 @@ export const addCustomerCreatedTask = async ({ payload: { req: req ? parseReqForAction(req) : undefined, internalCustomerId, - org, + orgId: org.id, env, }, }); }; export const handleCustomerCreated = async ({ - db, - logger, + ctx, data, }: { - db: DrizzleCli; - logger: any; - data: any; + ctx: AutumnContext; + data: HandleCustomerCreatedData; }) => { - const { req, internalCustomerId, org, env } = data; + const { req, internalCustomerId } = data; + const { db, org, env } = ctx; - let customer = await CusService.getFull({ + const customer = await CusService.getFull({ db, idOrInternalId: internalCustomerId, orgId: org.id, env, }); - let action = constructAction({ + const action = constructAction({ org, env, customer, diff --git a/server/src/internal/analytics/handlers/handleProductsUpdated.ts b/server/src/internal/analytics/handlers/handleProductsUpdated.ts index 7c4c38416..db6432988 100644 --- a/server/src/internal/analytics/handlers/handleProductsUpdated.ts +++ b/server/src/internal/analytics/handlers/handleProductsUpdated.ts @@ -2,15 +2,14 @@ import { ActionType, type AppEnv, type AuthType, - createdAtToVersion, cusProductToProduct, type FullCusProduct, type FullProduct, notNullish, type Organization, } from "@autumn/shared"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; import { sendSvixEvent } from "@/external/svix/svixHelpers.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { ActionService } from "@/internal/analytics/ActionService.js"; import { constructAction, @@ -19,7 +18,7 @@ import { import { getSingleEntityResponse } from "@/internal/api/entities/getEntityUtils.js"; import { CusService } from "@/internal/customers/CusService.js"; import { RELEVANT_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js"; -import { getCustomerDetails } from "@/internal/customers/cusUtils/getCustomerDetails.js"; +import { getApiCustomer } from "@/internal/customers/cusUtils/apiCusUtils/getApiCustomer.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js"; import { JobName } from "@/queue/JobName.js"; @@ -66,8 +65,10 @@ export const addProductsUpdatedWebhookTask = async ({ payload: { req: req ? parseReqForAction(req) : undefined, internalCustomerId, - org, + orgId: org.id, env, + // org, + // env, customerId, cusProduct, scheduledCusProduct, @@ -90,18 +91,16 @@ export const addProductsUpdatedWebhookTask = async ({ }; export const handleProductsUpdated = async ({ - db, - logger, + ctx, data, }: { - db: DrizzleCli; - logger: any; + ctx: AutumnContext; data: { req: Partial; actionDetails: ActionDetails; internalCustomerId: string; - org: Organization; - env: AppEnv; + // org: Organization; + // env: AppEnv; customerId: string; product: FullProduct; scenario: string; @@ -110,15 +109,10 @@ export const handleProductsUpdated = async ({ deletedCusProduct?: FullCusProduct; }; }) => { - const { - req, - org, - env, - scenario, - cusProduct, - scheduledCusProduct, - deletedCusProduct, - } = data; + const { req, scenario, cusProduct, scheduledCusProduct, deletedCusProduct } = + data; + + const { db, org, env, logger } = ctx; // Product: const product = cusProduct.product; @@ -126,8 +120,8 @@ export const handleProductsUpdated = async ({ const customer = await CusService.getFull({ db, idOrInternalId: data.customerId || data.internalCustomerId, - orgId: data.org.id, - env: data.env, + orgId: org.id, + env: env, inStatuses: RELEVANT_STATUSES, entityId: cusProduct.internal_entity_id || undefined, }); @@ -138,20 +132,10 @@ export const handleProductsUpdated = async ({ env, }); - const apiVersion = createdAtToVersion({ - createdAt: org.created_at || Date.now(), - }); - - const cusDetails = await getCustomerDetails({ - db, - customer: customer, - org, - env, - features, - logger, - cusProducts: customer.customer_products, + const apiCustomer = await getApiCustomer({ + ctx, + fullCus: customer, expand: [], - apiVersion, }); const productRes = await getProductResponse({ @@ -199,12 +183,10 @@ export const handleProductsUpdated = async ({ let entityRes = null; if (notNullish(customer?.entity)) { entityRes = await getSingleEntityResponse({ + ctx, entityId: customer.entity!.id, - org, - env, fullCus: customer, entity: customer.entity!, - features, }); } @@ -215,7 +197,7 @@ export const handleProductsUpdated = async ({ eventType: "customer.products.updated", data: { scenario, - customer: cusDetails, + customer: apiCustomer, entity: entityRes, updated_product: productRes, }, diff --git a/server/src/internal/analytics/runActionHandlerTask.ts b/server/src/internal/analytics/runActionHandlerTask.ts index 498103bc2..965ade707 100644 --- a/server/src/internal/analytics/runActionHandlerTask.ts +++ b/server/src/internal/analytics/runActionHandlerTask.ts @@ -1,54 +1,47 @@ -import { DrizzleCli } from "@/db/initDrizzle.js"; +import type { Job, Queue } from "bullmq"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { JobName } from "@/queue/JobName.js"; import { getLock, releaseLock } from "@/queue/lockUtils.js"; -import { Queue } from "bullmq"; -import { Job } from "bullmq"; -import { handleProductsUpdated } from "./handlers/handleProductsUpdated.js"; import { handleCustomerCreated } from "./handlers/handleCustomerCreated.js"; +import { handleProductsUpdated } from "./handlers/handleProductsUpdated.js"; export const runActionHandlerTask = async ({ queue, job, - logger, - db, useBackup, + ctx, }: { queue: Queue; job: Job; - logger: any; - db: DrizzleCli; useBackup: boolean; + ctx: AutumnContext; }) => { - let payload = job.data; - let internalCustomerId = payload.internalCustomerId; - let lockKey = `action:${internalCustomerId}`; + const payload = job.data; + const internalCustomerId = payload.internalCustomerId; + const lockKey = `action:${internalCustomerId}`; + + const { logger } = ctx; try { - let lock = await getLock({ queue, job, lockKey, useBackup }); + const lock = await getLock({ queue, job, lockKey, useBackup }); if (!lock) return; switch (job.name) { case JobName.HandleProductsUpdated: await handleProductsUpdated({ - db, - logger, + ctx, data: payload, }); break; case JobName.HandleCustomerCreated: await handleCustomerCreated({ - db, - logger, + ctx, data: payload, }); break; } } catch (error: any) { - logger.error("Error processing action handler job:", { - // jobName: job.name, - // payload, - message: error.message, - }); + logger.error(`Error processing action handler job: ${error.message}`); } finally { await releaseLock({ lockKey, useBackup }); } diff --git a/server/src/internal/api/apiRouter.ts b/server/src/internal/api/apiRouter.ts index c0b60695d..47731ced0 100644 --- a/server/src/internal/api/apiRouter.ts +++ b/server/src/internal/api/apiRouter.ts @@ -17,7 +17,7 @@ import { handleGetOrg } from "../orgs/handlers/handleGetOrg.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"; +import { expressEntityRouter } from "./entities/entityRouter.js"; import { checkRouter } from "./entitled/checkRouter.js"; import { eventsRouter } from "./events/eventRouter.js"; import { usageRouter } from "./events/usageRouter.js"; @@ -44,7 +44,7 @@ apiRouter.use("/features", featureRouter); apiRouter.use("/internal_features", internalFeatureRouter); apiRouter.use("/usage", usageRouter); -apiRouter.use("/entities", entityRouter); +apiRouter.use("/entities", expressEntityRouter); apiRouter.use("/migrations", migrationRouter); // REWARDS diff --git a/server/src/internal/api/batch/CusBatchService.ts b/server/src/internal/api/batch/CusBatchService.ts index edfed86cf..0d1270f3c 100644 --- a/server/src/internal/api/batch/CusBatchService.ts +++ b/server/src/internal/api/batch/CusBatchService.ts @@ -1,6 +1,7 @@ import { type ApiVersionClass, type AppEnv, + AuthType, CusExpand, type CusProductStatus, type Feature, @@ -10,7 +11,8 @@ import { import type { NodeClickHouseClient } from "@clickhouse/client/dist/client.js"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { RELEVANT_STATUSES } from "@/internal/customers/cusProducts/CusProductService.js"; -import { getCustomerDetails } from "../../customers/cusUtils/getCustomerDetails.js"; +import { getApiCustomer } from "@/internal/customers/cusUtils/apiCusUtils/getApiCustomer.js"; +import { generateId } from "@/utils/genUtils.js"; import { getPaginatedFullCusQuery } from "../../customers/getFullCusQuery.js"; export class CusBatchService { @@ -95,20 +97,24 @@ export class CusBatchService { const customer = normalizedCustomer as FullCustomer; const cusProducts = customer.customer_products || []; - const customerDetails = await getCustomerDetails({ - db, - customer, - features, - org, - env, - params: {}, - logger: console, - cusProducts, + const apiCustomer = await getApiCustomer({ + ctx: { + db, + org, + env, + features, + logger, + apiVersion, + id: generateId("local_req"), + isPublic: false, + authType: AuthType.Unknown, + timestamp: Date.now(), + }, + fullCus: customer, expand: expand, - apiVersion: apiVersion, }); - finals.push(customerDetails); + finals.push(apiCustomer); } catch (error) { console.error(`Failed to process customer ${result.id}:`, error); } @@ -124,7 +130,7 @@ export class CusBatchService { const normalizeTimestamp = (value: any): number => { if (typeof value === "string") { const parsed = parseInt(value, 10); - return isNaN(parsed) ? Date.now() : parsed; + return Number.isNaN(parsed) ? Date.now() : parsed; } return typeof value === "number" ? value : Date.now(); }; diff --git a/server/src/internal/api/entities/entityRouter.ts b/server/src/internal/api/entities/entityRouter.ts index bab88c220..5801330f7 100644 --- a/server/src/internal/api/entities/entityRouter.ts +++ b/server/src/internal/api/entities/entityRouter.ts @@ -1,24 +1,23 @@ import { Router } from "express"; - -import { routeHandler } from "@/utils/routerUtils.js"; import { CusService } from "@/internal/customers/CusService.js"; -import { handleGetEntity } from "./handlers/handleGetEntity.js"; +import { routeHandler } from "@/utils/routerUtils.js"; import { handlePostEntityRequest } from "../../entities/handlers/handleCreateEntity/handleCreateEntity.js"; import { handleDeleteEntity } from "./handlers/handleDeleteEntity.js"; +import { handleGetEntity } from "./handlers/handleGetEntity.js"; -export const entityRouter: Router = Router({ mergeParams: true }); +export const expressEntityRouter: Router = Router({ mergeParams: true }); // List entityes -entityRouter.get("", (req: any, res: any) => +expressEntityRouter.get("", (req: any, res: any) => routeHandler({ req, res, action: "listEntities", handler: async (req, res) => { const customerId = String(req.params.customer_id); - let { orgId, env } = req; + const { orgId, env } = req; - let customer = await CusService.getFull({ + const customer = await CusService.getFull({ db: req.db, idOrInternalId: customerId, orgId, @@ -34,10 +33,14 @@ entityRouter.get("", (req: any, res: any) => ); // 1. Create entity -entityRouter.post("", handlePostEntityRequest); +expressEntityRouter.post("", handlePostEntityRequest); // 2. Delete entity -entityRouter.delete("/:entity_id", handleDeleteEntity); +expressEntityRouter.delete("/:entity_id", handleDeleteEntity); // 3. Get entity -entityRouter.get("/:entity_id", handleGetEntity); +expressEntityRouter.get("/:entity_id", handleGetEntity); + +// export const entityRouter = new Hono(); + +// entityRouter.get("/:entity_id", ...handleGetEntity); diff --git a/server/src/internal/api/entities/getEntityUtils.ts b/server/src/internal/api/entities/getEntityUtils.ts index d6100f8fd..c84a2fd56 100644 --- a/server/src/internal/api/entities/getEntityUtils.ts +++ b/server/src/internal/api/entities/getEntityUtils.ts @@ -1,44 +1,34 @@ import { - type ApiCusProduct, - ApiVersion, - ApiVersionClass, - type AppEnv, + type ApiEntity, type Entity, type EntityExpand, - type EntityResponse, ErrCode, - type Feature, type FullCusProduct, type FullCustomer, notNullish, - type Organization, type Subscription, } from "@autumn/shared"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { getCusWithCache } from "@/internal/customers/cusCache/getCusWithCache.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { CusService } from "@/internal/customers/CusService.js"; +import { getApiCusPlans } from "@/internal/customers/cusUtils/apiCusUtils/getApiCusPlan/getApiCusPlans.js"; import { getCusFeaturesResponse } from "@/internal/customers/cusUtils/cusFeatureResponseUtils/getCusFeaturesResponse.js"; -import { processFullCusProducts } from "@/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.js"; import RecaseError from "@/utils/errorUtils.js"; import { nullish } from "@/utils/genUtils.js"; export const getSingleEntityResponse = async ({ + ctx, entityId, - org, - env, fullCus, entity, - features, withAutumnId = false, }: { + ctx: AutumnContext; entityId: string; - org: Organization; - env: AppEnv; fullCus: FullCustomer; entity: Entity; - features: Feature[]; withAutumnId?: boolean; }) => { - const apiVersion = new ApiVersionClass(ApiVersion.V1_2); + const { org, env, apiVersion } = ctx; if (!entity) { throw new RecaseError({ @@ -70,16 +60,24 @@ export const getSingleEntityResponse = async ({ ), ); - const { main, addOns } = await processFullCusProducts({ - fullCusProducts: entityCusProducts, - entity, - subs: entitySubs, - org, - apiVersion, - features, - }); + const fullEntity = structuredClone(fullCus); + fullEntity.customer_products = entityCusProducts; + fullEntity.subscriptions = entitySubs; - const products: ApiCusProduct[] = [...main, ...addOns]; + // const { main, addOns } = await processFullCusProducts({ + // fullCusProducts: entityCusProducts, + // entity, + // subs: entitySubs, + // org, + // apiVersion, + // features, + // }); + + // const products: ApiCusProduct[] = [...main, ...addOns]; + const { apiCusPlans } = await getApiCusPlans({ + ctx, + fullCus: fullEntity, + }); const cusFeatures = await getCusFeaturesResponse({ cusProducts: entityCusProducts, @@ -96,47 +94,52 @@ export const getSingleEntityResponse = async ({ // feature_id: entity.feature_id, customer_id: fullCus.id || fullCus.internal_id, env, - products, + plans: apiCusPlans, features: cusFeatures, - }; + } satisfies ApiEntity; }; export const getEntityResponse = async ({ - db, + ctx, entityIds, - org, - env, customerId, expand, entityId, withAutumnId = false, - apiVersion, - features, - logger, skipCache = false, }: { - db: DrizzleCli; + ctx: AutumnContext; entityIds: string[]; - org: Organization; - env: AppEnv; customerId: string; expand?: EntityExpand[]; entityId?: string; withAutumnId?: boolean; - apiVersion: number; - features: Feature[]; - logger: any; skipCache?: boolean; }) => { - const fullCus = await getCusWithCache({ + // const fullCus = await getCusWithCache({ + // db, + // idOrInternalId: customerId, + // org, + // env, + // expand, + // entityId, + // logger, + // skipCache, + // }); + + // don't use cache anymore? + + const { org, env, features, db } = ctx; + + const fullCus = await CusService.getFull({ db, idOrInternalId: customerId, - org, + orgId: org.id, env, - expand, entityId, - logger, - skipCache, + expand, + withSubs: true, + withEntities: true, }); if (!fullCus) { @@ -147,7 +150,7 @@ export const getEntityResponse = async ({ }); } - const entityResponses: EntityResponse[] = []; + const entityResponses: ApiEntity[] = []; for (const entityId of entityIds) { const entity = fullCus.entities.find( @@ -163,12 +166,10 @@ export const getEntityResponse = async ({ } const entityResponse = await getSingleEntityResponse({ + ctx, entityId, - org, - env, fullCus, entity, - features, withAutumnId, }); diff --git a/server/src/internal/api/entities/handlers/handleGetEntity.ts b/server/src/internal/api/entities/handlers/handleGetEntity.ts index 025af4018..93e2067f7 100644 --- a/server/src/internal/api/entities/handlers/handleGetEntity.ts +++ b/server/src/internal/api/entities/handlers/handleGetEntity.ts @@ -1,8 +1,6 @@ -import { ApiEntitySchema, EntityExpand, LegacyVersion } from "@autumn/shared"; +import { ApiEntitySchema, EntityExpand } from "@autumn/shared"; import { invoicesToResponse } from "@/internal/invoices/invoiceUtils.js"; -import { OrgService } from "@/internal/orgs/OrgService.js"; import { routeHandler } from "@/utils/routerUtils.js"; -import { orgToVersion } from "@/utils/versionUtils/legacyVersionUtils.js"; import { parseEntityExpand } from "../entityUtils.js"; import { getEntityResponse } from "../getEntityUtils.js"; @@ -16,32 +14,16 @@ export const handleGetEntity = async (req: any, res: any) => const customerId = req.params.customer_id as string; const expand = parseEntityExpand(req.query.expand); - const { env, db, logger, features } = req; + const { logger } = req; - const org = await OrgService.getFromReq(req); - const apiVersion = orgToVersion({ - org, - reqApiVersion: - req.apiVersion >= LegacyVersion.v1_1 - ? req.apiVersion - : LegacyVersion.v1_2, + const { entities, invoices } = await getEntityResponse({ + ctx: req, + entityIds: [entityId], + customerId, + expand, + entityId, }); - // const start = performance.now(); - const { entities, customer, fullEntities, invoices } = - await getEntityResponse({ - db, - entityIds: [entityId], - org, - env, - customerId, - expand, - entityId, - apiVersion, - features, - logger, - }); - const entity = entities[0]; const withInvoices = expand.includes(EntityExpand.Invoices); diff --git a/server/src/internal/api/entitled/checkRouter.ts b/server/src/internal/api/entitled/checkRouter.ts index 506a02b56..e82b2cd13 100644 --- a/server/src/internal/api/entitled/checkRouter.ts +++ b/server/src/internal/api/entitled/checkRouter.ts @@ -196,20 +196,3 @@ checkRouter.post("", async (req: any, res: any) => { handleRequestError({ req, error, res, action: "Failed to GET entitled" }); } }); - -// let features = [feature, ...creditSystems]; -// let balanceObj: any, featureToUse: any; -// try { -// balanceObj = balances.length > 0 ? balances[0] : null; - -// featureToUse = -// notNullish(balanceObj) && balanceObj.feature_id !== feature.id -// ? features.find((f) => f.id === balanceObj.feature_id) -// : creditSystems.length > 0 -// ? creditSystems[0] -// : feature; -// } catch (error) { -// logger.error(`/check: failed to get balance & feature to use`, error); -// } - -// 3. If with preview, get preview diff --git a/server/src/internal/api/entitled/getCheckPreview.ts b/server/src/internal/api/entitled/getCheckPreview.ts index 1b863de12..4e0ea62b1 100644 --- a/server/src/internal/api/entitled/getCheckPreview.ts +++ b/server/src/internal/api/entitled/getCheckPreview.ts @@ -1,4 +1,5 @@ import { + cusProductToProduct, type Feature, FeaturePreviewScenario, type FullCusProduct, @@ -6,7 +7,6 @@ import { type FullProduct, } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { fullCusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js"; import { @@ -43,7 +43,7 @@ export const getCheckPreview = async ({ ); const cusOwnedProducts = mainCusProds.map((cp: FullCusProduct) => - fullCusProductToProduct(cp), + cusProductToProduct({ cusProduct: cp }), ); sortProductsByPrice({ products: cusOwnedProducts }); diff --git a/server/src/internal/api/entitled/handleCheckV0.ts b/server/src/internal/api/entitled/handleCheckV0.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts index 42e323a00..b5b77285b 100644 --- a/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts +++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/getContUseInvoiceItems.ts @@ -1,37 +1,34 @@ -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; - import { - AttachReplaceable, + type AttachReplaceable, BillingType, - FullCusProduct, - FullCustomerEntitlement, - FullEntitlement, + type FullCusProduct, + type FullCustomerEntitlement, + type FullEntitlement, getFeatureInvoiceDescription, - intervalsSame, - PreviewLineItem, - Price, + type PreviewLineItem, + type Price, } from "@autumn/shared"; -import Stripe from "stripe"; -import { attachParamsToProduct } from "../convertAttachParams.js"; -import { - getBillingType, - getPriceEntitlement, - getPriceForOverage, -} from "@/internal/products/prices/priceUtils.js"; +import { Decimal } from "decimal.js"; +import type Stripe from "stripe"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { findCusEnt } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js"; import { getExistingUsageFromCusProducts, getRelatedCusPrice, } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; -import { subToAutumnInterval } from "@/external/stripe/utils.js"; -import { Decimal } from "decimal.js"; -import { getCurContUseItems } from "@/internal/invoices/previewItemUtils/getCurContUseItems.js"; -import { intervalsAreSame } from "../getAttachConfig.js"; import { newPriceToInvoiceDescription } from "@/internal/invoices/invoiceFormatUtils.js"; +import { getCurContUseItems } from "@/internal/invoices/previewItemUtils/getCurContUseItems.js"; import { getDefaultPriceStr } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js"; import { priceToUsageModel } from "@/internal/products/prices/priceUtils/convertPrice.js"; -import { priceToContUseItem } from "./priceToContUseItem.js"; import { shouldProrate } from "@/internal/products/prices/priceUtils/prorationConfigUtils.js"; +import { + getBillingType, + getPriceEntitlement, + getPriceForOverage, +} from "@/internal/products/prices/priceUtils.js"; +import { attachParamsToProduct } from "../convertAttachParams.js"; +import { intervalsAreSame } from "../getAttachConfig.js"; +import { priceToContUseItem } from "./priceToContUseItem.js"; export const getContUseNewItems = async ({ price, @@ -56,14 +53,14 @@ export const getContUseNewItems = async ({ internalEntityId: attachParams.internalEntityId, }); - let description = newPriceToInvoiceDescription({ + const description = newPriceToInvoiceDescription({ org, price, product: newProduct, quantity: usage, }); - if (usage == 0) { + if (usage === 0) { return { price_id: price.id, price: getDefaultPriceStr({ org, price, ent, features }), @@ -80,8 +77,8 @@ export const getContUseNewItems = async ({ prevCusEnt && !shouldProrate(price.proration_config?.on_decrease) ) { - let isDowngrade = ent.allowance! > prevCusEnt.entitlement.allowance!; - let prevBalance = prevCusEnt.balance!; + const isDowngrade = ent.allowance! > prevCusEnt.entitlement.allowance!; + const prevBalance = prevCusEnt.balance!; if (isDowngrade && prevBalance < 0) { overage = new Decimal(prevBalance).abs().toNumber(); @@ -89,8 +86,8 @@ export const getContUseNewItems = async ({ } } - let amount = getPriceForOverage(price, overage); - let description = getFeatureInvoiceDescription({ + const amount = getPriceForOverage(price, overage); + const description = getFeatureInvoiceDescription({ feature: ent.feature, usage: usage, prodName: newProduct.name, @@ -130,24 +127,24 @@ export const getContUseInvoiceItems = async ({ }) : []; - let newEnts = product.entitlements; - let oldItems: PreviewLineItem[] = []; - let newItems: PreviewLineItem[] = []; - let replaceables: AttachReplaceable[] = []; + const newEnts = product.entitlements; + const oldItems: PreviewLineItem[] = []; + const newItems: PreviewLineItem[] = []; + const replaceables: AttachReplaceable[] = []; for (const price of product.prices) { - let billingType = getBillingType(price.config); + const billingType = getBillingType(price.config); if (billingType !== BillingType.InArrearProrated) { continue; } - let ent = getPriceEntitlement(price, newEnts); - let prevCusEnt = findCusEnt({ + const ent = getPriceEntitlement(price, newEnts); + const prevCusEnt = findCusEnt({ cusEnts, feature: ent.feature, }); - let prevCusPrice = prevCusEnt + const prevCusPrice = prevCusEnt ? getRelatedCusPrice(prevCusEnt, cusPrices)! : undefined; @@ -176,7 +173,7 @@ export const getContUseInvoiceItems = async ({ (item) => item.price_id === prevCusPrice?.price.id, ); - let { + const { oldItem, newItems: newItems_, replaceables: replaceables_, diff --git a/server/src/internal/customers/attach/attachUtils/getContUseItems/priceToContUseItem.ts b/server/src/internal/customers/attach/attachUtils/getContUseItems/priceToContUseItem.ts index 42515df36..ca01f99d7 100644 --- a/server/src/internal/customers/attach/attachUtils/getContUseItems/priceToContUseItem.ts +++ b/server/src/internal/customers/attach/attachUtils/getContUseItems/priceToContUseItem.ts @@ -1,25 +1,25 @@ -import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { - FullCustomerEntitlement, - FullEntitlement, - PreviewLineItem, - Price, + cusProductsToCusPrices, + type FullCustomerEntitlement, + type FullEntitlement, + type PreviewLineItem, + type Price, } from "@autumn/shared"; -import Stripe from "stripe"; -import { attachParamsToProduct } from "../convertAttachParams.js"; +import { Decimal } from "decimal.js"; +import type Stripe from "stripe"; +import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; +import type { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js"; import { getExistingUsageFromCusProducts, getRelatedCusPrice, } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; -import { getContUseDowngradeItems } from "./getContUseDowngradeItems.js"; -import { shouldProrate } from "@/internal/products/prices/priceUtils/prorationConfigUtils.js"; -import { getContUseUpgradeItems } from "./getContUseUpgradeItems.js"; import { priceToInvoiceItem } from "@/internal/products/prices/priceUtils/priceToInvoiceItem.js"; -import { Decimal } from "decimal.js"; +import { shouldProrate } from "@/internal/products/prices/priceUtils/prorationConfigUtils.js"; import { notNullish } from "@/utils/genUtils.js"; -import { cusProductsToCusPrices } from "@autumn/shared"; -import { findStripeItemForPrice } from "@/external/stripe/stripeSubUtils/stripeSubItemUtils.js"; -import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js"; +import { attachParamsToProduct } from "../convertAttachParams.js"; +import { getContUseDowngradeItems } from "./getContUseDowngradeItems.js"; +import { getContUseUpgradeItems } from "./getContUseUpgradeItems.js"; export const priceToContUseItem = async ({ price, @@ -74,7 +74,7 @@ export const priceToContUseItem = async ({ : shouldProrate(price.proration_config?.on_increase); // 1. Get current usage - let curUsage = getExistingUsageFromCusProducts({ + const curUsage = getExistingUsageFromCusProducts({ entitlement: ent, cusProducts, entities, @@ -132,8 +132,8 @@ export const priceToContUseItem = async ({ // Clean up items // 1. If old item and new item same, remove both - let oldAmount = res.oldItem?.amount!; - let newAmount = res.newItem?.amount!; + const oldAmount = res.oldItem?.amount ?? 0; + const newAmount = res.newItem?.amount ?? 0; if (new Decimal(oldAmount).add(newAmount).eq(0)) { return { diff --git a/server/src/internal/customers/change-product/scheduleUtils.ts b/server/src/internal/customers/change-product/scheduleUtils.ts index 91083d574..6f55aafed 100644 --- a/server/src/internal/customers/change-product/scheduleUtils.ts +++ b/server/src/internal/customers/change-product/scheduleUtils.ts @@ -1,37 +1,4 @@ -import { - getStripeSchedules, - getStripeSubs, -} from "@/external/stripe/stripeSubUtils.js"; -import { - AppEnv, - AttachScenario, - FullCusProduct, - intervalsSame, - Organization, - Product, -} from "@autumn/shared"; -import Stripe from "stripe"; -import { fullCusProductToProduct } from "../cusProducts/cusProductUtils.js"; -import { - ACTIVE_STATUSES, - CusProductService, -} from "../cusProducts/CusProductService.js"; - -import { getExistingCusProducts } from "../cusProducts/cusProductUtils/getExistingCusProducts.js"; -import { isFreeProduct } from "@/internal/products/productUtils.js"; - -import { getFilteredScheduleItems } from "./scheduleUtils/getFilteredScheduleItems.js"; -import { updateScheduledSubWithNewItems } from "./scheduleUtils/updateScheduleWithNewItems.js"; -import { - addCurMainProductToSchedule, - getOtherCusProductsOnSub, -} from "./scheduleUtils/cancelScheduledFreeProduct.js"; -import { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; -import { DrizzleCli } from "@/db/initDrizzle.js"; -import { ExtendedRequest } from "@/utils/models/Request.js"; -import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js"; -import { notNullish } from "@/utils/genUtils.js"; -import { subToAutumnInterval } from "@/external/stripe/utils.js"; +import type { FullCusProduct } from "@autumn/shared"; export const getPricesForCusProduct = ({ cusProduct, diff --git a/server/src/internal/customers/change-product/scheduleUtils/getFilteredScheduleItems.ts b/server/src/internal/customers/change-product/scheduleUtils/getFilteredScheduleItems.ts index 9b49fe06f..ba4b754c0 100644 --- a/server/src/internal/customers/change-product/scheduleUtils/getFilteredScheduleItems.ts +++ b/server/src/internal/customers/change-product/scheduleUtils/getFilteredScheduleItems.ts @@ -1,8 +1,6 @@ +import { cusProductToProduct, type FullCusProduct } from "@autumn/shared"; import { getPricesForCusProduct } from "../scheduleUtils.js"; - -import { FullCusProduct } from "@autumn/shared"; -import { ScheduleObj } from "./ScheduleObj.js"; -import { fullCusProductToProduct } from "../../cusProducts/cusProductUtils.js"; +import type { ScheduleObj } from "./ScheduleObj.js"; export const getFilteredScheduleItems = ({ scheduleObj, cusProducts, @@ -11,7 +9,7 @@ export const getFilteredScheduleItems = ({ cusProducts: (FullCusProduct | undefined)[]; }) => { const { schedule, prices } = scheduleObj; - let scheduleItems = schedule.phases[0].items; + const scheduleItems = schedule.phases[0].items; let curPrices: any[] = []; for (const cusProduct of cusProducts) { @@ -20,14 +18,14 @@ export const getFilteredScheduleItems = ({ } } - let products = cusProducts + const products = cusProducts .filter((cp): cp is FullCusProduct => !!cp) - .map((cp: FullCusProduct) => fullCusProductToProduct(cp)); + .map((cp: FullCusProduct) => cusProductToProduct({ cusProduct: cp })); return scheduleItems.filter((scheduleItem: any) => { - let stripePrice = prices.find((price) => price.id === scheduleItem.price); + const stripePrice = prices.find((price) => price.id === scheduleItem.price); - let inCurProduct = + const inCurProduct = curPrices.some( (price) => price.config?.stripe_price_id === scheduleItem.price || diff --git a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts index 6cbe487d9..68e54c1ed 100644 --- a/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts +++ b/server/src/internal/customers/cusProducts/cusEnts/cusEntUtils.ts @@ -223,10 +223,10 @@ export const getUnlimitedAndUsageAllowed = ({ ); const usageAllowed = cusEnts.some( - (ent) => - ent.internal_feature_id === internalFeatureId && - ent.usage_allowed && - (includeUsageLimit ? nullish(ent.entitlement.usage_limit) : true), + (ce) => + ce.internal_feature_id === internalFeatureId && + ce.usage_allowed && + (includeUsageLimit ? nullish(ce.entitlement.usage_limit) : true), ); return { unlimited, usageAllowed }; diff --git a/server/src/internal/customers/cusProducts/cusProductUtils.ts b/server/src/internal/customers/cusProducts/cusProductUtils.ts index 43cf86d8f..223f4de2e 100644 --- a/server/src/internal/customers/cusProducts/cusProductUtils.ts +++ b/server/src/internal/customers/cusProducts/cusProductUtils.ts @@ -1,12 +1,10 @@ import { ACTIVE_STATUSES, - ApiCusProductSchema, + ApiCusPlanSchema, ApiVersion, type ApiVersionClass, - type AppEnv, AttachScenario, CusProductStatus, - cusProductToPrices, type Entity, type FixedPriceConfig, type FullCusProduct, @@ -20,7 +18,6 @@ import { } 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 { addProductsUpdatedWebhookTask } from "@/internal/analytics/handlers/handleProductsUpdated.js"; import { ProductService } from "@/internal/products/ProductService.js"; import { @@ -28,7 +25,7 @@ import { getUsageTier, } from "@/internal/products/prices/priceUtils.js"; import { isDefaultTrialFullProduct } from "@/internal/products/productUtils/classifyProduct.js"; -import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; +import { isFreeProduct } from "@/internal/products/productUtils.js"; import { notNullish, nullish } from "@/utils/genUtils.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; import { handleAddProduct } from "../attach/attachFunctions/addProductFlow/handleAddProduct.js"; @@ -38,80 +35,6 @@ import { CusProductService, RELEVANT_STATUSES } from "./CusProductService.js"; import { getRelatedCusEnt } from "./cusPrices/cusPriceUtils.js"; import { getExistingCusProducts } from "./cusProductUtils/getExistingCusProducts.js"; -// 1. Cancel cusProductSubscriptions -// CAN DELETE -export const cancelCusProductSubscriptions = async ({ - cusProduct, - org, - env, - excludeIds, - expireImmediately = true, - logger, - prorate = true, -}: { - cusProduct: FullCusProduct; - org: Organization; - env: AppEnv; - excludeIds?: string[]; - expireImmediately?: boolean; - logger: any; - prorate?: boolean; -}) => { - // 1. Cancel all subscriptions - const stripeCli = createStripeCli({ - org: org, - env: env, - }); - - let latestSubEnd: number | undefined; - if (cusProduct.subscription_ids && cusProduct.subscription_ids.length > 0) { - const stripeSubs = await getStripeSubs({ - stripeCli, - subIds: cusProduct.subscription_ids, - }); - - latestSubEnd = stripeSubs?.[0]?.items.data[0].current_period_end; - } - - const cancelStripeSub = async (subId: string) => { - if (excludeIds && excludeIds.includes(subId)) { - return; - } - - try { - if (expireImmediately) { - await stripeCli.subscriptions.cancel(subId, { - prorate: prorate, - }); - } else { - await stripeCli.subscriptions.update(subId, { - cancel_at: latestSubEnd || undefined, - cancel_at_period_end: latestSubEnd ? undefined : true, - }); - } - - logger.info(`Cancelled stripe subscription ${subId}, org: ${org.slug}`); - } catch (error: any) { - if (error.code !== "resource_missing") { - console.log( - `Error canceling stripe subscription ${error.code}: ${error.message}`, - ); - } // else subscription probably already cancelled - } - }; - - if (cusProduct.subscription_ids && cusProduct.subscription_ids.length > 0) { - const batchCancel = []; - for (const subId of cusProduct.subscription_ids) { - batchCancel.push(cancelStripeSub(subId)); - } - await Promise.all(batchCancel); - return true; - } - - return false; -}; - export const getDefaultProduct = async ({ req, productGroup, @@ -160,22 +83,6 @@ export const activateDefaultProduct = async ({ p.group === productGroup && !isDefaultTrialFullProduct({ product: p }), ); - // const defaultableProducts = { - // free: defaultProducts.filter( - // (p) => p.group === productGroup && isFreeProduct(p.prices), - // ), - // paid: defaultProducts.filter( - // (p) => - // p.group === productGroup && isDefaultTrialFullProduct({ product: p }), - // ), - // }; - - // if (defaultableProducts.paid.length > 0) { - // defaultProd = defaultableProducts.paid[0]; - // } else if (defaultableProducts.free.length > 0) { - // defaultProd = defaultableProducts.free[0]; - // } - if (!defaultProd) return false; if (curCusProduct?.internal_product_id === defaultProd.internal_id) { @@ -221,96 +128,6 @@ export const activateDefaultProduct = async ({ }); return true; - - // if (!isDefaultTrial) { - // const existingDefaultProduct = fullCus.customer_products.find( - // (cp) => - // cp.product.internal_id === defaultProd?.internal_id && - // ACTIVE_STATUSES.includes(cp.status), - // ); - - // if (existingDefaultProduct) { - // logger.info( - // `Default product ${defaultProd?.name} already exists for customer`, - // ); - // return false; - // } - - // await handleAddProduct({ - // req, - // attachParams: newCusToAttachParams({ - // req, - // newCus: fullCus, - // products: [defaultProd], - // stripeCli, - // }), - // }); - - // return true; - // } else if (isDefaultTrial && defaultableProducts.free.length > 0) { - // defaultProd = defaultableProducts.free[0]; - - // // Check if the free default product already exists to prevent duplicates - // const existingFreeProduct = fullCus.customer_products.find( - // (cp) => - // cp.product.internal_id === defaultProd!.internal_id && - // (cp.status === CusProductStatus.Active || - // cp.status === CusProductStatus.PastDue), - // ); - - // if (existingFreeProduct) { - // logger.info( - // `Free default product ${defaultProd?.name} already exists for customer`, - // ); - // return false; - // } - - // await handleAddProduct({ - // req, - // attachParams: newCusToAttachParams({ - // req, - // newCus: fullCus, - // products: [defaultProd], - // stripeCli, - // }), - // config: getDefaultAttachConfig(), - // }); - - // return true; - // } -}; - -export const expireAndActivate = async ({ - req, - cusProduct, - fullCus, -}: { - req: ExtendedRequest; - cusProduct: FullCusProduct; - fullCus: FullCustomer; -}) => { - const { db, org, env, logger } = req; - // 1. Expire current product - await CusProductService.update({ - db, - cusProductId: cusProduct.id, - updates: { status: CusProductStatus.Expired, ended_at: Date.now() }, - }); - - // Check if it's one time product - const prices = cusProductToPrices({ cusProduct }); - const product = cusProduct.product; - const isOneOffOrAddOn = product.is_add_on || isOneOff(prices); - - if (isOneOffOrAddOn || notNullish(cusProduct.internal_entity_id)) { - return; - } - - await activateDefaultProduct({ - req, - productGroup: cusProduct.product.group, - fullCus, - }); }; export const activateFutureProduct = async ({ @@ -356,45 +173,8 @@ export const activateFutureProduct = async ({ }); return futureProduct; - - // if (subIsPrematurelyCanceled(subscription)) { - // console.log( - // " 🔔 Subscription prematurely canceled, deleting scheduled products" - // ); - - // await deleteScheduledIds({ - // stripeCli, - // scheduledIds: futureProduct.scheduled_ids || [], - // }); - // await CusProductService.delete({ - // db, - // cusProductId: futureProduct.id, - // }); - // return false; - // } else { - // await CusProductService.update({ - // db, - // cusProductId: futureProduct.id, - // updates: { status: CusProductStatus.Active }, - // }); - - // await addProductsUpdatedWebhookTask({ - // req, - // internalCustomerId: cusProduct.internal_customer_id, - // org, - // env, - // customerId: null, - // scenario: AttachScenario.New, - // cusProduct: futureProduct, - // logger, - // }); - - // return true; - // } }; -// GET CUS ENTS FROM CUS PRODUCTS - export const processFullCusProduct = ({ cusProduct, subs, @@ -512,7 +292,7 @@ export const processFullCusProduct = ({ }; } - return ApiCusProductSchema.parse({ + return ApiCusPlanSchema.parse({ id: cusProduct.product.id, name: cusProduct.product.name, group: cusProduct.product.group || null, @@ -552,15 +332,6 @@ export const processFullCusProduct = ({ } }; -// GET CUSTOMER PRODUCT & ORG IN PARALLEL -export const fullCusProductToProduct = (cusProduct: FullCusProduct) => { - return { - ...cusProduct.product, - prices: cusProduct.customer_prices.map((cp) => cp.price), - entitlements: cusProduct.customer_entitlements.map((ce) => ce.entitlement), - }; -}; - export const searchCusProducts = ({ productId, internalProductId, diff --git a/server/src/internal/customers/cusRouter.ts b/server/src/internal/customers/cusRouter.ts index a866e3cef..09dd1efcd 100644 --- a/server/src/internal/customers/cusRouter.ts +++ b/server/src/internal/customers/cusRouter.ts @@ -9,7 +9,7 @@ import { CusSearchService } from "@/internal/customers/CusSearchService.js"; import { OrgService } from "@/internal/orgs/OrgService.js"; import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; import { handleBatchCustomers } from "../api/batch/handlers/handleBatchCustomers.js"; -import { entityRouter } from "../api/entities/entityRouter.js"; +import { expressEntityRouter } from "../api/entities/entityRouter.js"; import { toSuccessUrl } from "../orgs/orgUtils/convertOrgUtils.js"; import { CusService } from "./CusService.js"; import { handleAddCouponToCus } from "./handlers/handleAddCouponToCus.js"; @@ -63,11 +63,6 @@ expressCusRouter.post( expressCusRouter.post("/:customer_id/balances", handleUpdateBalances); -// cusRouter.post( -// "/customer_products/:customer_product_id", -// handleCusProductExpired -// ); - expressCusRouter.get( "/:customer_id/billing_portal", async (req: any, res: any) => { @@ -143,7 +138,7 @@ expressCusRouter.post( expressCusRouter.post("/:customer_id/coupons/:coupon_id", handleAddCouponToCus); -expressCusRouter.use("/:customer_id/entities", entityRouter); +expressCusRouter.use("/:customer_id/entities", expressEntityRouter); expressCusRouter.post("/:customer_id/transfer", handleTransferProduct); diff --git a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusFeature/apiCusFeatureUtils.ts b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusFeature/apiCusFeatureUtils.ts index 22b645660..4d36be7a7 100644 --- a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusFeature/apiCusFeatureUtils.ts +++ b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusFeature/apiCusFeatureUtils.ts @@ -1,33 +1,38 @@ import { type ApiCusFeature, type ApiCusRollover, - ApiFeatureType, + type ApiFeature, cusEntToKey, - type EntInterval, + entIntvToResetIntv, type FullCusEntWithFullCusProduct, getRolloverFields, notNullish, + ResetInterval, + toIntervalCountResponse, } from "@autumn/shared"; -import { getCusFeatureType } from "@/internal/features/featureUtils.js"; export const cusEntsToInterval = ({ cusEnts, }: { cusEnts: FullCusEntWithFullCusProduct[]; }): { - interval: EntInterval | "multiple" | null; - interval_count: number | null; + interval: ResetInterval | null; + interval_count: number | undefined; } => { const cusEntKeys = cusEnts.map((cusEnt) => cusEntToKey({ cusEnt })); const uniqueCusEntKeys = [...new Set(cusEntKeys)]; if (uniqueCusEntKeys.length === 1) { return { - interval: cusEnts[0].entitlement.interval || null, - interval_count: cusEnts[0].entitlement.interval_count, + interval: entIntvToResetIntv({ + entInterval: cusEnts[0].entitlement.interval, + }), + interval_count: toIntervalCountResponse({ + intervalCount: cusEnts[0].entitlement.interval_count, + }), }; } - return { interval: "multiple", interval_count: null }; + return { interval: null, interval_count: undefined }; }; export const cusEntsToNextResetAt = ({ @@ -76,39 +81,66 @@ export const cusEntsToRollovers = ({ export const getBooleanApiCusFeature = ({ cusEnts, + apiFeature, }: { cusEnts: FullCusEntWithFullCusProduct[]; + apiFeature?: ApiFeature; }): ApiCusFeature => { const feature = cusEnts[0].entitlement.feature; return { - id: feature.id, - type: ApiFeatureType.Static, - name: feature.name, + feature: apiFeature, + feature_id: feature.id, + + unlimited: false, + + starting_balance: 0, balance: 0, usage: 0, - included_usage: 0, - next_reset_at: null, - unlimited: false, - overage_allowed: false, - }; + + resets_at: null, + reset_interval: ResetInterval.OneOff, + reset_interval_count: undefined, + + breakdown: undefined, + rollovers: undefined, + + // Old + // id: feature.id, + // type: ApiFeatureType.Static, + // name: feature.name, + // balance: 0, + // usage: 0, + // included_usage: 0, + // next_reset_at: null, + // unlimited: false, + // overage_allowed: false, + } as ApiCusFeature; }; export const getUnlimitedApiCusFeature = ({ + apiFeature, cusEnts, }: { + apiFeature?: ApiFeature; cusEnts: FullCusEntWithFullCusProduct[]; }): ApiCusFeature => { const feature = cusEnts[0].entitlement.feature; return { - id: feature.id, - type: getCusFeatureType({ feature }), - name: feature.name, + feature: apiFeature, + feature_id: feature.id, + + unlimited: true, + + starting_balance: 0, balance: 0, usage: 0, - included_usage: 0, - next_reset_at: null, - unlimited: true, - overage_allowed: false, + + resets_at: null, + reset_interval: ResetInterval.OneOff, + reset_interval_count: undefined, + + breakdown: undefined, + rollovers: undefined, }; }; diff --git a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusFeature/getApiCusFeature.ts b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusFeature/getApiCusFeature.ts index 0550625d5..e8f6457cd 100644 --- a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusFeature/getApiCusFeature.ts +++ b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusFeature/getApiCusFeature.ts @@ -6,10 +6,11 @@ import { type ApiCusFeatureBreakdown, ApiCusFeatureBreakdownSchema, ApiCusFeatureSchema, + CusExpand, cusEntToBalance, cusEntToIncludedUsage, cusEntToKey, - cusEntToUsageLimit, + cusEntToMaxPurchase, type Feature, FeatureType, getCusEntBalance, @@ -20,7 +21,6 @@ import { import { Decimal } from "decimal.js"; import type { RequestContext } from "@/honoUtils/HonoEnv.js"; import { getUnlimitedAndUsageAllowed } from "@/internal/customers/cusProducts/cusEnts/cusEntUtils.js"; -import { getCusFeatureType } from "@/internal/features/featureUtils.js"; import { cusEntsToInterval, cusEntsToNextResetAt, @@ -60,11 +60,12 @@ const cusEntsToBreakdown = ({ cusEnts, feature, }); + breakdown.push( ApiCusFeatureBreakdownSchema.parse({ ...breakdownItem, - interval, - interval_count, + reset_interval: interval, + reset_interval_count: interval_count, }), ); } @@ -85,21 +86,27 @@ export const getApiCusFeature = ({ }) => { const entityId = fullCus.entity?.id; + const apiFeature = ctx.expand.includes(CusExpand.Feature) + ? toApiFeature({ feature }) + : undefined; + // 1. If feature is boolean if (feature.type === FeatureType.Boolean) { return getBooleanApiCusFeature({ cusEnts, + apiFeature, }); } const { unlimited, usageAllowed } = getUnlimitedAndUsageAllowed({ cusEnts: cusEnts, internalFeatureId: feature.internal_id, + includeUsageLimit: false, }); // 2. If feature is unlimited if (unlimited) { - return getUnlimitedApiCusFeature({ cusEnts: cusEnts }); + return getUnlimitedApiCusFeature({ apiFeature, cusEnts }); } const totalBalanceWithRollovers = sumValues( @@ -130,8 +137,8 @@ export const getApiCusFeature = ({ const nextResetAt = cusEntsToNextResetAt({ cusEnts }); - const totalUsageLimit = sumValues( - cusEnts.map((cusEnt) => cusEntToUsageLimit({ cusEnt })), + const totalMaxPurchase = sumValues( + cusEnts.map((cusEnt) => cusEntToMaxPurchase({ cusEnt })), ); const totalIncludedUsage = sumValues( @@ -154,30 +161,53 @@ export const getApiCusFeature = ({ const rollovers = cusEntsToRollovers({ cusEnts, entityId }); - const apiFeature = toApiFeature({ feature }); + // const apiFeature = toApiFeature({ feature }); const { data: apiCusFeature, error } = ApiCusFeatureSchema.safeParse({ - id: feature.id, - name: feature.name, - type: getCusFeatureType({ feature }), - balance: totalBalanceWithRollovers, - usage: totalUsage, - included_usage: totalIncludedUsage, - usage_limit: - totalUsageLimit === totalIncludedUsage ? undefined : totalUsageLimit, - next_reset_at: nextResetAt, + feature: ctx.expand.includes(CusExpand.Feature) ? apiFeature : undefined, + + feature_id: feature.id, + unlimited: false, - overage_allowed: usageAllowed, - interval, - interval_count, - rollovers, - credit_schema: - apiFeature.credit_schema?.map((credit) => ({ - feature_id: credit.metered_feature_id, - credit_amount: credit.credit_cost, - })) || undefined, + + starting_balance: totalIncludedUsageWithRollovers, + balance: totalBalanceWithRollovers, + + usage: totalUsage, + + resets_at: nextResetAt, + reset_interval: interval, + reset_interval_count: interval_count !== 1 ? interval_count : undefined, + + // Max purchase... + max_purchase: totalMaxPurchase > 0 ? totalMaxPurchase : undefined, + pay_per_use: usageAllowed || undefined, breakdown: cusEntsToBreakdown({ ctx, fullCus, cusEnts }), + rollovers, + + // Old stuff... + // id: feature.id, + // name: feature.name, + // type: getCusFeatureType({ feature }), + // balance: totalBalanceWithRollovers, + // usage: totalUsage, + // included_usage: totalIncludedUsage, + // usage_limit: + // totalUsageLimit === totalIncludedUsage ? undefined : totalUsageLimit, + // next_reset_at: nextResetAt, + // unlimited: false, + // overage_allowed: usageAllowed, + // interval, + // interval_count, + // rollovers, + // credit_schema: + // apiFeature.credit_schema?.map((credit) => ({ + // feature_id: credit.metered_feature_id, + // credit_amount: credit.credit_cost, + // })) || undefined, + + // breakdown: cusEntsToBreakdown({ ctx, fullCus, cusEnts }), }); if (error) throw error; diff --git a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusPlan/getApiCusPlan.ts b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusPlan/getApiCusPlan.ts new file mode 100644 index 000000000..1e6e5e3e2 --- /dev/null +++ b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusPlan/getApiCusPlan.ts @@ -0,0 +1,120 @@ +import { + ApiCusPlanSchema, + CusExpand, + type CusProductLegacyData, + CusProductStatus, + cusProductToPlanStatus, + cusProductToProduct, + type Entity, + type FullCusProduct, + type FullCustomer, + isTrialing, + type Subscription, +} from "@autumn/shared"; +import type { RequestContext } from "@/honoUtils/HonoEnv.js"; +import { getPlanResponse } from "@/internal/products/productUtils/productResponseUtils/getPlanResponse.js"; + +export const getApiCusPlan = async ({ + ctx, + fullCus, + cusProduct, + entities, +}: { + ctx: RequestContext; + fullCus: FullCustomer; + cusProduct: FullCusProduct; + entities?: Entity[]; +}) => { + const trialing = + cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now(); + + const fullProduct = cusProductToProduct({ cusProduct }); + + const subId = cusProduct.subscription_ids?.[0]; + const autumnSub = fullCus.subscriptions?.find( + (s) => s.id === subId || (s as Subscription).stripe_id === subId, + ); + + let stripeSubData: { + current_period_start: number | null; + current_period_end: number | null; + } = { + current_period_start: null, + current_period_end: null, + }; + + if (autumnSub) { + stripeSubData = { + current_period_end: autumnSub?.current_period_end + ? autumnSub.current_period_end * 1000 + : null, + current_period_start: autumnSub?.current_period_start + ? autumnSub.current_period_start * 1000 + : null, + }; + } + + if (!subId && trialing) { + stripeSubData = { + current_period_start: cusProduct.starts_at, + current_period_end: cusProduct.trial_ends_at || null, + }; + } + + const status = cusProductToPlanStatus({ status: cusProduct.status }); + + // Check if we should expand the plan object + const shouldExpandPlan = ctx.expand.includes(CusExpand.Plan); + const apiPlan = shouldExpandPlan + ? await getPlanResponse({ + product: fullProduct, + features: ctx.features, + }) + : undefined; + + const apiCusPlan = ApiCusPlanSchema.parse({ + plan: apiPlan, + + plan_id: fullProduct.id, + add_on: fullProduct.is_add_on, + + status, + past_due: cusProduct.status === CusProductStatus.PastDue, + canceled_at: cusProduct.canceled_at || null, + expires_at: cusProduct.ended_at || null, + + trial_ends_at: isTrialing({ cusProduct }) ? cusProduct.trial_ends_at : null, + started_at: cusProduct.starts_at, + quantity: cusProduct.quantity, + current_period_start: stripeSubData?.current_period_start || null, + current_period_end: stripeSubData?.current_period_end || null, + }); + + return { + data: apiCusPlan, + legacyData: { + subscription_id: subId || undefined, + options: cusProduct.options, + features: ctx.features, + } satisfies CusProductLegacyData, + }; +}; + +// const apiCusProduct = ApiCusProductSchema.parse({ +// id: fullProduct.id, +// name: fullProduct.name, +// group: fullProduct.group || null, +// status: trialing ? CusProductStatus.Trialing : cusProduct.status, +// canceled_at: cusProduct.canceled_at || null, + +// is_default: fullProduct.is_default || false, +// is_add_on: fullProduct.is_add_on || false, +// version: fullProduct.version, +// quantity: cusProduct.quantity, + +// started_at: cusProduct.starts_at, +// entity_id: entity?.id || cusProduct.entity_id || undefined, + +// ...stripeSubData, +// items: v2Product.items, +// }); diff --git a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusPlan/getApiCusPlans.ts b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusPlan/getApiCusPlans.ts new file mode 100644 index 000000000..9775757dc --- /dev/null +++ b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusPlan/getApiCusPlans.ts @@ -0,0 +1,79 @@ +import { + ACTIVE_STATUSES, + type ApiCusPlan, + type CusProductLegacyData, + type CusProductStatus, + type FullCustomer, +} from "@autumn/shared"; +import type { RequestContext } from "@/honoUtils/HonoEnv.js"; +import { getApiCusPlan } from "./getApiCusPlan.js"; + +const mergeCusPlanResponses = ({ + cusPlanResponses, +}: { + cusPlanResponses: ApiCusPlan[]; +}) => { + const getPlanKey = (cp: ApiCusPlan) => { + const status = ACTIVE_STATUSES.includes(cp.status as CusProductStatus) + ? "active" + : cp.status; + return `${cp.plan_id}:${status}`; + }; + + const record: Record = {}; + + for (const curr of cusPlanResponses) { + const key = getPlanKey(curr); + const latest = record[key]; + + const currStartedAt = curr.started_at; + + record[key] = { + ...(latest || curr), + canceled_at: curr.canceled_at + ? curr.canceled_at + : latest?.canceled_at || null, + started_at: latest?.started_at + ? Math.min(latest?.started_at, currStartedAt) + : currStartedAt, + quantity: (latest?.quantity || 0) + (curr?.quantity || 0), + }; + } + + return Object.values(record); +}; + +export const getApiCusPlans = async ({ + ctx, + fullCus, +}: { + ctx: RequestContext; + fullCus: FullCustomer; +}) => { + // Process full cus products + const apiCusPlans: ApiCusPlan[] = []; + + const cusProducts = fullCus.customer_products; + + const legacyData: Record = {}; + for (const cusProduct of cusProducts) { + const processed = await getApiCusPlan({ + cusProduct, + ctx, + fullCus, + entities: fullCus.entities, + }); + + apiCusPlans.push(processed.data); + legacyData[processed.data.plan_id] = processed.legacyData; + } + + const merged = mergeCusPlanResponses({ + cusPlanResponses: apiCusPlans, + }); + + return { + apiCusPlans: merged, + legacyData, + }; +}; diff --git a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusProduct/getApiCusProduct.ts b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusProduct/getApiCusProduct.ts deleted file mode 100644 index 76de8874f..000000000 --- a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusProduct/getApiCusProduct.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { - ApiCusProductSchema, - type CusProductLegacyData, - CusProductStatus, - cusProductToProduct, - type Entity, - type FullCusProduct, - type FullCustomer, - type Subscription, -} from "@autumn/shared"; -import type { RequestContext } from "@/honoUtils/HonoEnv.js"; -import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js"; - -// const prices = cusProduct.customer_prices.map((cp) => { -// const price = cp.price; - -// if (price.config?.type === PriceType.Fixed) { -// const config = price.config as FixedPriceConfig; -// return { -// amount: config.amount, -// interval: config.interval, -// }; -// } else { -// const config = price.config as UsagePriceConfig; -// const priceOptions = getPriceOptions(price, cusProduct.options); -// const usageTier = getUsageTier(price, priceOptions?.quantity!); -// const cusEnt = getRelatedCusEnt({ -// cusPrice: cp, -// cusEnts: cusProduct.customer_entitlements, -// }); - -// const ent = cusEnt?.entitlement; - -// const singleTier = -// ent?.allowance === 0 && config.usage_tiers.length === 1; - -// if (singleTier) { -// return { -// amount: usageTier.amount, -// interval: config.interval, -// quantity: priceOptions?.quantity, -// }; -// } else { -// // Add allowance to tiers -// const allowance = ent?.allowance; -// let tiers; - -// if (notNullish(allowance) && allowance! > 0) { -// tiers = [ -// { -// to: allowance, -// amount: 0, -// }, -// ...config.usage_tiers.map((tier) => { -// const isLastTier = tier.to === -1 || tier.to === TierInfinite; -// return { -// to: isLastTier ? tier.to : Number(tier.to) + allowance!, -// amount: tier.amount, -// }; -// }), -// ]; -// } else { -// tiers = config.usage_tiers.map((tier) => { -// const isLastTier = tier.to === -1 || tier.to === TierInfinite; -// return { -// to: isLastTier ? tier.to : Number(tier.to) + allowance!, -// amount: tier.amount, -// }; -// }); -// } - -// return { -// tiers: tiers, -// name: "", -// quantity: priceOptions?.quantity, -// }; -// } -// } -// }); - -export const getApiCusProduct = async ({ - ctx, - fullCus, - cusProduct, - entities, -}: { - ctx: RequestContext; - fullCus: FullCustomer; - cusProduct: FullCusProduct; - entities?: Entity[]; -}) => { - const { org } = ctx; - const trialing = - cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now(); - - const fullProduct = cusProductToProduct({ cusProduct }); - const v2Product = await getProductResponse({ - product: fullProduct, - features: ctx.features, - fullCus, - currency: org.default_currency || "usd", - options: cusProduct.options, - withDisplay: false, - }); - - const entity = entities?.find( - (e) => e.internal_id === cusProduct.internal_entity_id, - ); - - const subId = cusProduct.subscription_ids?.[0]; - const autumnSub = fullCus.subscriptions?.find( - (s) => s.id === subId || (s as Subscription).stripe_id === subId, - ); - - let stripeSubData = {}; - if (autumnSub) { - stripeSubData = { - current_period_end: autumnSub?.current_period_end - ? autumnSub.current_period_end * 1000 - : null, - current_period_start: autumnSub?.current_period_start - ? autumnSub.current_period_start * 1000 - : null, - }; - } - - if (!subId && trialing) { - stripeSubData = { - current_period_start: cusProduct.starts_at, - current_period_end: cusProduct.trial_ends_at, - }; - } - - const apiCusProduct = ApiCusProductSchema.parse({ - id: fullProduct.id, - name: fullProduct.name, - group: fullProduct.group || null, - status: trialing ? CusProductStatus.Trialing : cusProduct.status, - canceled_at: cusProduct.canceled_at || null, - - is_default: fullProduct.is_default || false, - is_add_on: fullProduct.is_add_on || false, - version: fullProduct.version, - quantity: cusProduct.quantity, - - started_at: cusProduct.starts_at, - entity_id: entity?.id || cusProduct.entity_id || undefined, - - ...stripeSubData, - items: v2Product.items, - }); - - return { - data: apiCusProduct, - legacyData: { - subscription_id: subId || undefined, - } satisfies CusProductLegacyData, - }; - - // if (subIds && subIds.length > 0 && apiVersion.gte(ApiVersion.V0_2)) { - // const baseSub = subs?.find( - // (s) => s.id === subIds[0] || (s as Subscription).stripe_id === subIds[0], - // ); - // stripeSubData = { - // current_period_end: baseSub?.current_period_end - // ? baseSub.current_period_end * 1000 - // : null, - // current_period_start: baseSub?.current_period_start - // ? baseSub.current_period_start * 1000 - // : null, - // }; - // } - - // if (!subIds && trialing) { - // stripeSubData = { - // current_period_start: cusProduct.starts_at, - // current_period_end: cusProduct.trial_ends_at, - // }; - // } - - // if (apiVersion.gte(ApiVersion.V1_1)) { - // if ((!subIds || subIds.length === 0) && trialing) { - // stripeSubData = { - // current_period_start: cusProduct.starts_at, - // current_period_end: cusProduct.trial_ends_at, - // }; - // } - - // const fullProduct = fullCusProductToProduct(cusProduct); - // const v2Product = await getProductResponse({ - // product: fullProduct, - // features, - // withDisplay: false, - // options: cusProduct.options, - // }); - - // return ApiCusProductSchema.parse({ - // id: fullProduct.id, - // name: fullProduct.name, - // group: fullProduct.group || null, - // status: trialing ? CusProductStatus.Trialing : cusProduct.status, - // canceled_at: cusProduct.canceled_at || null, - - // is_default: fullProduct.is_default || false, - // is_add_on: fullProduct.is_add_on || false, - // version: fullProduct.version, - // quantity: cusProduct.quantity, - - // started_at: cusProduct.starts_at, - // entity_id: entity?.id || cusProduct.entity_id || undefined, - - // ...stripeSubData, - // items: v2Product.items, - // }); - // } else { - // const cusProductResponse = { - // id: cusProduct.product.id, - // name: cusProduct.product.name, - // group: cusProduct.product.group, - // status: trialing ? CusProductStatus.Trialing : cusProduct.status, - // created_at: cusProduct.created_at, - // canceled_at: cusProduct.canceled_at, - // processor: { - // type: cusProduct.processor?.type, - // subscription_id: cusProduct.processor?.subscription_id || null, - // }, - // subscription_ids: cusProduct.subscription_ids || [], - // prices: prices, - // starts_at: cusProduct.starts_at, - - // ...stripeSubData, - // }; - - // return cusProductResponse; - // } -}; diff --git a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusProduct/getApiCusProducts.ts b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusProduct/getApiCusProducts.ts deleted file mode 100644 index fa951327d..000000000 --- a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCusProduct/getApiCusProducts.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - ACTIVE_STATUSES, - type ApiCusProduct, - type CusProductLegacyData, - type CusProductStatus, - type FullCustomer, -} from "@autumn/shared"; -import type { RequestContext } from "@/honoUtils/HonoEnv.js"; -import { getApiCusProduct } from "./getApiCusProduct.js"; - -const mergeCusProductResponses = ({ - cusProductResponses, -}: { - cusProductResponses: ApiCusProduct[]; -}) => { - const getProductKey = (product: ApiCusProduct) => { - const status = ACTIVE_STATUSES.includes(product.status as CusProductStatus) - ? "active" - : product.status; - return `${product.id}:${status}`; - }; - - const record: Record = {}; - - for (const curr of cusProductResponses) { - const key = getProductKey(curr); - const latest = record[key]; - - const currStartedAt = curr.started_at; - - record[key] = { - ...(latest || curr), - version: Math.max(latest?.version || 1, curr?.version || 1), - canceled_at: curr.canceled_at - ? curr.canceled_at - : latest?.canceled_at || null, - started_at: latest?.started_at - ? Math.min(latest?.started_at, currStartedAt) - : currStartedAt, - quantity: (latest?.quantity || 0) + (curr?.quantity || 0), - }; - } - - return Object.values(record); -}; - -export const getApiCusProducts = async ({ - ctx, - fullCus, -}: { - ctx: RequestContext; - fullCus: FullCustomer; -}) => { - // Process full cus products - const apiCusProducts: ApiCusProduct[] = []; - - // const inStatuses = ctx.org.config.include_past_due - // ? [CusProductStatus.Active, CusProductStatus.PastDue] - // : [CusProductStatus.Active]; - - // const cusProducts = fullCus.customer_products.filter((cp) => - // inStatuses.includes(cp.status), - // ); - const cusProducts = fullCus.customer_products; - - const legacyData: Record = {}; - for (const cusProduct of cusProducts) { - const processed = await getApiCusProduct({ - cusProduct, - ctx, - fullCus, - entities: fullCus.entities, - }); - - apiCusProducts.push(processed.data); - legacyData[processed.data.id] = processed.legacyData; - } - - const merged = mergeCusProductResponses({ - cusProductResponses: apiCusProducts, - }); - - return { - apiCusProducts: merged, - legacyData, - }; -}; diff --git a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomer.ts b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomer.ts index 8f992872a..40d5481b4 100644 --- a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomer.ts +++ b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomer.ts @@ -3,25 +3,22 @@ import { type ApiCustomer, ApiCustomerSchema, applyResponseVersionChanges, - type CusExpand, type CustomerLegacyData, type FullCustomer, } from "@autumn/shared"; import { z } from "zod/v4"; import type { RequestContext } from "@/honoUtils/HonoEnv.js"; import { getApiCusFeatures } from "./getApiCusFeature/getApiCusFeatures.js"; -import { getApiCusProducts } from "./getApiCusProduct/getApiCusProducts.js"; +import { getApiCusPlans } from "./getApiCusPlan/getApiCusPlans.js"; import { getApiCustomerExpand } from "./getApiCustomerExpand.js"; export const getApiCustomer = async ({ ctx, fullCus, - expand, withAutumnId = false, }: { ctx: RequestContext; fullCus: FullCustomer; - expand: CusExpand[]; withAutumnId?: boolean; }) => { const apiCusFeatures = await getApiCusFeatures({ @@ -29,8 +26,8 @@ export const getApiCustomer = async ({ fullCus, }); - const { apiCusProducts, legacyData: cusProductLegacyData } = - await getApiCusProducts({ + const { apiCusPlans, legacyData: cusProductLegacyData } = + await getApiCusPlans({ ctx, fullCus, }); @@ -38,7 +35,6 @@ export const getApiCustomer = async ({ const apiCusExpand = await getApiCustomerExpand({ ctx, fullCus, - expand, }); const apiCustomer = ApiCustomerSchema.extend({ @@ -56,16 +52,16 @@ export const getApiCustomer = async ({ env: fullCus.env, metadata: fullCus.metadata, - products: apiCusProducts, + plans: apiCusPlans, + features: apiCusFeatures, + ...apiCusExpand, }); return applyResponseVersionChanges({ input: apiCustomer, - legacyData: { - cusProductLegacyData, - }, + legacyData: { cusProductLegacyData }, targetVersion: ctx.apiVersion, resource: AffectedResource.Customer, }); diff --git a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerExpand.ts b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerExpand.ts index ca88eeb1b..4954af271 100644 --- a/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerExpand.ts +++ b/server/src/internal/customers/cusUtils/apiCusUtils/getApiCustomerExpand.ts @@ -1,6 +1,6 @@ import { ApiBaseEntitySchema, - type ApiCustomerExpand, + type ApiCusExpand, CusExpand, type FullCusProduct, type FullCustomer, @@ -15,13 +15,12 @@ import { getCusUpcomingInvoice } from "../cusResponseUtils/getCusUpcomingInvoice export const getApiCustomerExpand = async ({ ctx, fullCus, - expand, }: { ctx: AutumnContext; fullCus: FullCustomer; - expand: CusExpand[]; -}): Promise => { - const { org, env, db, logger } = ctx; +}): Promise => { + const { org, env, db, logger, expand } = ctx; + console.log("Expand: ", expand); const getCusTrialsUsed = () => { if (expand.includes(CusExpand.TrialsUsed)) { return fullCus.trials_used; @@ -43,6 +42,8 @@ export const getApiCustomerExpand = async ({ }) : undefined; + const cusExpand = expand as CusExpand[]; + const [rewards, upcomingInvoice, referrals, paymentMethod] = await Promise.all([ getCusRewards({ @@ -52,25 +53,25 @@ export const getApiCustomerExpand = async ({ subIds: fullCus.customer_products.flatMap( (cp: FullCusProduct) => cp.subscription_ids || [], ), - expand, + expand: cusExpand, }), getCusUpcomingInvoice({ db, org, env, fullCus, - expand, + expand: cusExpand, }), getCusReferrals({ db, fullCus, - expand, + expand: cusExpand, }), getCusPaymentMethodRes({ org, env, fullCus, - expand, + expand: cusExpand, }), ]); diff --git a/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts b/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts deleted file mode 100644 index 2f2e6cbd2..000000000 --- a/server/src/internal/customers/cusUtils/cusProductResponseUtils/getCusProductResponse.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { - ApiCusProductSchema, - ApiVersion, - type ApiVersionClass, - CusProductStatus, - type Entity, - type Feature, - type FixedPriceConfig, - type FullCusProduct, - type Organization, - PriceType, - type Subscription, - TierInfinite, - type UsagePriceConfig, -} from "@autumn/shared"; -import { - getPriceOptions, - getUsageTier, -} from "@/internal/products/prices/priceUtils.js"; -import { getProductResponse } from "@/internal/products/productUtils/productResponseUtils/getProductResponse.js"; -import { notNullish } from "@/utils/genUtils.js"; -import { getRelatedCusEnt } from "../../cusProducts/cusPrices/cusPriceUtils.js"; -import { fullCusProductToProduct } from "../../cusProducts/cusProductUtils.js"; - -export const getCusProductResponse = async ({ - cusProduct, - subs, - org, - apiVersion, - features, - entity, -}: { - cusProduct: FullCusProduct; - org: Organization; - subs?: Subscription[]; - apiVersion: ApiVersionClass; - features: Feature[]; - entity?: Entity; -}) => { - // Process prices - - const prices = cusProduct.customer_prices.map((cp) => { - const price = cp.price; - - if (price.config?.type === PriceType.Fixed) { - const config = price.config as FixedPriceConfig; - return { - amount: config.amount, - interval: config.interval, - }; - } else { - const config = price.config as UsagePriceConfig; - const priceOptions = getPriceOptions(price, cusProduct.options); - const usageTier = getUsageTier(price, priceOptions?.quantity!); - const cusEnt = getRelatedCusEnt({ - cusPrice: cp, - cusEnts: cusProduct.customer_entitlements, - }); - - const ent = cusEnt?.entitlement; - - const singleTier = - ent?.allowance === 0 && config.usage_tiers.length === 1; - - if (singleTier) { - return { - amount: usageTier.amount, - interval: config.interval, - quantity: priceOptions?.quantity, - }; - } else { - // Add allowance to tiers - const allowance = ent?.allowance; - let tiers; - - if (notNullish(allowance) && allowance! > 0) { - tiers = [ - { - to: allowance, - amount: 0, - }, - ...config.usage_tiers.map((tier) => { - const isLastTier = tier.to === -1 || tier.to === TierInfinite; - return { - to: isLastTier ? tier.to : Number(tier.to) + allowance!, - amount: tier.amount, - }; - }), - ]; - } else { - tiers = config.usage_tiers.map((tier) => { - const isLastTier = tier.to === -1 || tier.to === TierInfinite; - return { - to: isLastTier ? tier.to : Number(tier.to) + allowance!, - amount: tier.amount, - }; - }); - } - - return { - tiers: tiers, - name: "", - quantity: priceOptions?.quantity, - }; - } - } - }); - - const trialing = - cusProduct.trial_ends_at && cusProduct.trial_ends_at > Date.now(); - - const subIds = cusProduct.subscription_ids; - let stripeSubData = {}; - - if (subIds && subIds.length > 0 && apiVersion.gte(ApiVersion.V0_2)) { - const baseSub = subs?.find( - (s) => s.id === subIds[0] || (s as Subscription).stripe_id === subIds[0], - ); - stripeSubData = { - current_period_end: baseSub?.current_period_end - ? baseSub.current_period_end * 1000 - : null, - current_period_start: baseSub?.current_period_start - ? baseSub.current_period_start * 1000 - : null, - }; - } - - if (!subIds && trialing) { - stripeSubData = { - current_period_start: cusProduct.starts_at, - current_period_end: cusProduct.trial_ends_at, - }; - } - - if (apiVersion.gte(ApiVersion.V1_1)) { - if ((!subIds || subIds.length === 0) && trialing) { - stripeSubData = { - current_period_start: cusProduct.starts_at, - current_period_end: cusProduct.trial_ends_at, - }; - } - - const fullProduct = fullCusProductToProduct(cusProduct); - const v2Product = await getProductResponse({ - product: fullProduct, - features, - withDisplay: false, - options: cusProduct.options, - }); - - return ApiCusProductSchema.parse({ - id: fullProduct.id, - name: fullProduct.name, - group: fullProduct.group || null, - status: trialing ? CusProductStatus.Trialing : cusProduct.status, - canceled_at: cusProduct.canceled_at || null, - - is_default: fullProduct.is_default || false, - is_add_on: fullProduct.is_add_on || false, - version: fullProduct.version, - quantity: cusProduct.quantity, - - started_at: cusProduct.starts_at, - entity_id: entity?.id || cusProduct.entity_id || undefined, - - ...stripeSubData, - items: v2Product.items, - }); - } else { - const cusProductResponse = { - id: cusProduct.product.id, - name: cusProduct.product.name, - group: cusProduct.product.group, - status: trialing ? CusProductStatus.Trialing : cusProduct.status, - created_at: cusProduct.created_at, - canceled_at: cusProduct.canceled_at, - processor: { - type: cusProduct.processor?.type, - subscription_id: cusProduct.processor?.subscription_id || null, - }, - subscription_ids: cusProduct.subscription_ids || [], - prices: prices, - starts_at: cusProduct.starts_at, - - ...stripeSubData, - }; - - return cusProductResponse; - } -}; diff --git a/server/src/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.ts b/server/src/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.ts deleted file mode 100644 index b5043e7d6..000000000 --- a/server/src/internal/customers/cusUtils/cusProductResponseUtils/processFullCusProducts.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { - ACTIVE_STATUSES, - type ApiCusProduct, - ApiVersion, - type ApiVersionClass, - type CusProductStatus, - type Entity, - type Feature, - type Organization, -} from "@autumn/shared"; -import { getCusProductResponse } from "./getCusProductResponse.js"; - -const mergeCusProductResponses = ({ - cusProductResponses, -}: { - cusProductResponses: ApiCusProduct[]; -}) => { - const getProductKey = (product: ApiCusProduct) => { - const status = ACTIVE_STATUSES.includes(product.status as CusProductStatus) - ? "active" - : product.status; - return `${product.id}:${status}`; - }; - - const record: Record = {}; - - for (const curr of cusProductResponses) { - const key = getProductKey(curr); - const latest = record[key]; - - const currStartedAt = curr.started_at; - - record[key] = { - ...(latest || curr), - version: Math.max(latest?.version || 1, curr?.version || 1), - canceled_at: curr.canceled_at - ? curr.canceled_at - : latest?.canceled_at || null, - started_at: latest?.started_at - ? Math.min(latest?.started_at, currStartedAt) - : currStartedAt, - quantity: (latest?.quantity || 0) + (curr?.quantity || 0), - }; - } - - return Object.values(record); -}; - -export const processFullCusProducts = async ({ - fullCusProducts, - subs, - org, - entity, - apiVersion, - features, -}: { - fullCusProducts: any; - subs: any; - org: Organization; - entity?: Entity; - apiVersion: ApiVersionClass; - features: Feature[]; -}) => { - // Process full cus products - let main = []; - let addOns = []; - for (const cusProduct of fullCusProducts) { - const processed = await getCusProductResponse({ - cusProduct, - subs, - org, - entity, - apiVersion, - features, - }); - - const isAddOn = cusProduct.product.is_add_on; - if (isAddOn) { - addOns.push(processed); - } else { - main.push(processed); - } - } - - if (apiVersion.gte(ApiVersion.V1_1)) { - main = mergeCusProductResponses({ - cusProductResponses: main as ApiCusProduct[], - }); - addOns = mergeCusProductResponses({ - cusProductResponses: addOns as ApiCusProduct[], - }); - } - - return { - main: main, - addOns: addOns, - }; -}; diff --git a/server/src/internal/customers/cusUtils/getCustomerDetails.ts b/server/src/internal/customers/cusUtils/getCustomerDetails.ts deleted file mode 100644 index 8e41888b7..000000000 --- a/server/src/internal/customers/cusUtils/getCustomerDetails.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { - ApiBaseEntitySchema, - ApiCusFeatureV2Schema, - ApiCustomerSchema, - ApiCustomerV2Schema, - ApiVersion, - type ApiVersionClass, - type AppEnv, - CusExpand, - CusProductStatus, - CustomerResponseSchema, - cusProductsToCusEnts, - cusProductsToCusPrices, - type Feature, - FeatureType, - type FullCusProduct, - type FullCustomer, - type Organization, - type RewardResponse, -} from "@autumn/shared"; -import type { DrizzleCli } from "@/db/initDrizzle.js"; -import { invoicesToResponse } from "@/internal/invoices/invoiceUtils.js"; -import { featuresToObject } from "./cusFeatureResponseUtils/balancesToFeatureResponse.js"; -import { getCusBalances } from "./cusFeatureResponseUtils/getCusBalances.js"; -import { processFullCusProducts } from "./cusProductResponseUtils/processFullCusProducts.js"; -import { getCusPaymentMethodRes } from "./cusResponseUtils/getCusPaymentMethodRes.js"; -import { getCusReferrals } from "./cusResponseUtils/getCusReferrals.js"; -import { getCusRewards } from "./cusResponseUtils/getCusRewards.js"; -import { getCusUpcomingInvoice } from "./cusResponseUtils/getCusUpcomingInvoice.js"; -import { getCusInvoices } from "./cusUtils.js"; - -export const getCustomerDetails = async ({ - db, - customer, - features, - org, - env, - params = {}, - logger, - cusProducts, - expand, - apiVersion, -}: { - db: DrizzleCli; - customer: FullCustomer; - features: Feature[]; - org: Organization; - env: AppEnv; - params?: any; - logger: any; - cusProducts: FullCusProduct[]; - expand: CusExpand[]; - apiVersion: ApiVersionClass; -}) => { - const withRewards = expand.includes(CusExpand.Rewards); - - const inStatuses = org.config.include_past_due - ? [CusProductStatus.Active, CusProductStatus.PastDue] - : [CusProductStatus.Active]; - - const cusEnts = cusProductsToCusEnts({ cusProducts, inStatuses }) as any; - - const balances = await getCusBalances({ - cusEntsWithCusProduct: cusEnts, - cusPrices: cusProductsToCusPrices({ cusProducts, inStatuses }), - org, - apiVersion, - }); - - const subIds = cusProducts.flatMap( - (cp: FullCusProduct) => cp.subscription_ids || [], - ); - - const subs = customer.subscriptions || []; - const { main, addOns } = await processFullCusProducts({ - fullCusProducts: cusProducts, - subs, - org, - apiVersion, - features, - }); - - if (apiVersion.gte(ApiVersion.V1_1)) { - let entList: any = balances.map((b) => { - const isBoolean = - features.find((f: Feature) => f.id === b.feature_id)?.type === - FeatureType.Boolean; - if (b.unlimited || isBoolean) { - return b; - } - - return ApiCusFeatureV2Schema.parse({ - ...b, - usage: b.used, - included_usage: b.allowance, - }); - }); - - const products: any = [...main, ...addOns]; - - if (apiVersion.gte(ApiVersion.V1_2)) { - entList = featuresToObject({ - features, - entList, - }); - } - - const withInvoices = expand.includes(CusExpand.Invoices); - - const rewards: RewardResponse | undefined = await getCusRewards({ - org, - env, - fullCus: customer, - subIds, - expand, - }); - - const upcomingInvoice = await getCusUpcomingInvoice({ - db, - org, - env, - fullCus: customer, - expand, - }); - - const referrals = await getCusReferrals({ - db, - fullCus: customer, - expand, - }); - - const paymentMethod = await getCusPaymentMethodRes({ - org, - env, - fullCus: customer, - expand, - }); - - const apiCustomerSchema = apiVersion.gte(ApiVersion.V1_2) - ? ApiCustomerSchema - : ApiCustomerV2Schema; - - const cusResponse = { - ...apiCustomerSchema.parse({ - ...customer, - stripe_id: customer.processor?.id, - features: entList, - products, - // invoices: withInvoices ? invoices : undefined, - - invoices: withInvoices - ? invoicesToResponse({ - invoices: customer.invoices || [], - logger, - }) - : undefined, - trials_used: expand.includes(CusExpand.TrialsUsed) - ? customer.trials_used - : undefined, - rewards: withRewards ? rewards : undefined, - metadata: customer.metadata, - entities: expand.includes(CusExpand.Entities) - ? customer.entities.map((e) => - ApiBaseEntitySchema.parse({ - id: e.id, - name: e.name, - customer_id: customer.id, - feature_id: e.feature_id, - created_at: e.created_at, - env: customer.env, - }), - ) - : undefined, - referrals, - payment_method: paymentMethod, - upcoming_invoice: upcomingInvoice, - }), - }; - - if (params?.with_autumn_id === "true") { - return { - ...cusResponse, - autumn_id: customer.internal_id, - }; - } else { - return cusResponse; - } - } else { - // Probably don't need items...? - // const withItems = org.config.api_version >= BREAK_API_VERSION; - // const withItems = apiVersion.gte(ApiVersion.V0_2); - - const processedInvoices = await getCusInvoices({ - db, - internalCustomerId: customer.internal_id, - invoices: customer.invoices, - limit: 20, - withItems: false, - features, - }); - - return { - customer: CustomerResponseSchema.parse(customer), - products: main, - add_ons: addOns, - entitlements: balances, - invoices: processedInvoices, - trials_used: expand.includes(CusExpand.TrialsUsed) - ? customer.trials_used - : undefined, - }; - } -}; diff --git a/server/src/internal/customers/handlers/handleCusProductExpired.ts b/server/src/internal/customers/handlers/handleCusProductExpired.ts deleted file mode 100644 index 34b530c43..000000000 --- a/server/src/internal/customers/handlers/handleCusProductExpired.ts +++ /dev/null @@ -1,211 +0,0 @@ -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, -} from "@/internal/customers/cusProducts/cusProductUtils.js"; -import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js"; -import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; -import type { ExtendedRequest } from "@/utils/models/Request.js"; -import { CusService } from "../CusService.js"; -export const expireCusProduct = async ({ - req, - cusProduct, // cus product to expire - fullCus, - expireImmediately = true, - prorate, -}: { - req: ExtendedRequest; - cusProduct: FullCusProduct; - fullCus: FullCustomer; - expireImmediately: boolean; - prorate: boolean; -}) => { - const { db, org, env, logger } = req; - logger.info("--------------------------------"); - logger.info( - `🔔 Expiring cutomer product (${ - expireImmediately ? "immediately" : "end of cycle" - })`, - ); - logger.info( - `Customer: ${fullCus.id || fullCus.internal_id} (${env}), Org: ${org.id}`, - ); - logger.info( - `Product: ${cusProduct.product.name}, Status: ${cusProduct.status}`, - ); - - // if (cusProduct.status == CusProductStatus.Scheduled) { - // await CusProductService.delete({ - // db, - // cusProductId: cusProduct.id, - // }); - // return; - // } - - // 1. If main product, can't expire if there's scheduled product - const isMain = !cusProduct.product.is_add_on; - const { curScheduledProduct: futureProduct } = getExistingCusProducts({ - product: cusProduct.product, - cusProducts: fullCus.customer_products, - internalEntityId: cusProduct.internal_entity_id, - }); - - if (isMain) { - if ( - cusProduct.canceled_at && - ACTIVE_STATUSES.includes(cusProduct.status) && - !expireImmediately - ) { - throw new RecaseError({ - message: `Product ${cusProduct.product.name} is already about to cancel at the end of cycle.`, - code: ErrCode.InvalidRequest, - statusCode: StatusCodes.BAD_REQUEST, - }); - } - - if ( - futureProduct && - !isFreeProduct(cusProductToPrices({ cusProduct: futureProduct })) - ) { - throw new RecaseError({ - message: `Please delete scheduled product ${futureProduct.product.name} first`, - code: ErrCode.InvalidRequest, - statusCode: StatusCodes.BAD_REQUEST, - }); - } - } - - // 2. If expire at cycle end, just cancel subscriptions - if (!expireImmediately) { - // 1. Check if already canceled - await cancelCusProductSubscriptions({ - cusProduct, - org, - env, - expireImmediately, - logger, - }); - - if (isOneOff(cusProduct.customer_prices.map((p) => p.price))) { - await CusProductService.update({ - db, - cusProductId: cusProduct.id, - updates: { status: CusProductStatus.Expired }, - }); - } else { - await CusProductService.update({ - db, - cusProductId: cusProduct.id, - updates: { canceled_at: Date.now() }, - }); - } - - return; - } - - if (cusProduct.product.is_add_on) { - await cancelCusProductSubscriptions({ - cusProduct, - org, - env, - logger, - }); - - await CusProductService.update({ - db, - cusProductId: cusProduct.id, - updates: { - status: CusProductStatus.Expired, - ended_at: Date.now(), - }, - }); - - return; - } - - // Remove scheduled products first... - if (futureProduct) { - await CusProductService.delete({ - db, - cusProductId: futureProduct.id, - }); - } - - logger.info(`Expiring current product: ${cusProduct.product.name}`); - await expireAndActivate({ - req, - cusProduct, - fullCus, - }); - - logger.info(`Cancelling stripe subscriptions`); - await cancelCusProductSubscriptions({ - cusProduct, - org, - env, - logger, - prorate, - }); - - return; -}; - -export const handleCusProductExpired = async (req: any, res: any) => { - try { - const { db } = req; - - const customerProductId = req.params.customer_product_id; - const cusProduct = await CusProductService.get({ - db, - id: customerProductId, - orgId: req.orgId, - env: req.env, - withCustomer: true, - }); - - if (!cusProduct) { - throw new RecaseError({ - message: `Cus product not found: ${customerProductId}`, - code: ErrCode.CusProductNotFound, - statusCode: 404, - }); - } - - const fullCus = await CusService.getFull({ - db, - idOrInternalId: - cusProduct.customer!.id || cusProduct.customer!.internal_id, - orgId: req.orgId, - env: req.env, - }); - - await expireCusProduct({ - req, - cusProduct, - fullCus, - expireImmediately: true, - prorate: true, - }); - - res.status(200).json({ message: "Product expired" }); - } catch (error) { - handleRequestError({ - req, - error, - res, - action: "update customer product", - }); - } -}; diff --git a/server/src/internal/customers/handlers/handleGetCustomer.ts b/server/src/internal/customers/handlers/handleGetCustomer.ts deleted file mode 100644 index 4c5fdcc06..000000000 --- a/server/src/internal/customers/handlers/handleGetCustomer.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { CusExpand, ErrCode, LegacyVersion } from "@autumn/shared"; -import { StatusCodes } from "http-status-codes"; -import { routeHandler } from "@/utils/routerUtils.js"; -import { orgToVersion } from "@/utils/versionUtils/legacyVersionUtils.js"; -import { getCusWithCache } from "../cusCache/getCusWithCache.js"; -import { parseCusExpand } from "../cusUtils/cusUtils.js"; -import { getCustomerDetails } from "../cusUtils/getCustomerDetails.js"; - -export const handleGetCustomer = async (req: any, res: any) => - routeHandler({ - req, - res, - action: "get customer", - handler: async () => { - const customerId = req.params.customer_id; - const { env, db, logger, org, features } = req; - const { expand } = req.query; - - const expandArray = parseCusExpand(expand); - - const apiVersion = orgToVersion({ - org, - reqApiVersion: req.apiVersion, - }); - - const getInvoices = apiVersion < LegacyVersion.v1_1; - if (getInvoices) expandArray.push(CusExpand.Invoices); - - logger.info(`getting customer ${customerId} for org ${org.slug}`); - const startTime = Date.now(); - const customer = await getCusWithCache({ - db, - idOrInternalId: customerId, - org, - env, - expand: expandArray, - allowNotFound: true, - logger, - }); - - logger.info(`get customer took ${Date.now() - startTime}ms`); - - if (!customer) { - logger.warn( - `GET /customers/${customerId}: not found | Org: ${org.slug}`, - ); - res.status(StatusCodes.NOT_FOUND).json({ - message: `Customer ${customerId} not found`, - code: ErrCode.CustomerNotFound, - }); - return; - } - - const cusData = await getCustomerDetails({ - db, - customer, - org, - env: req.env, - logger, - cusProducts: customer.customer_products, - expand: expandArray, - features, - apiVersion: req.apiVersion, - }); - - res.status(200).json(cusData); - }, - }); diff --git a/server/src/internal/customers/handlers/handleGetCustomerV2.ts b/server/src/internal/customers/handlers/handleGetCustomerV2.ts index 18b5cdeaf..988b160b8 100644 --- a/server/src/internal/customers/handlers/handleGetCustomerV2.ts +++ b/server/src/internal/customers/handlers/handleGetCustomerV2.ts @@ -1,6 +1,9 @@ import { + AffectedResource, + ApiVersion, backwardsChangeActive, CusExpand, + type EntityExpand, GetCustomerQuerySchema, V0_2_InvoicesAlwaysExpanded, } from "@autumn/shared"; @@ -9,12 +12,15 @@ import { getCusWithCache } from "../cusCache/getCusWithCache.js"; import { getApiCustomer } from "../cusUtils/apiCusUtils/getApiCustomer.js"; export const handleGetCustomerV2 = createRoute({ - query: GetCustomerQuerySchema, + versionedQuery: { + latest: GetCustomerQuerySchema, + [ApiVersion.V1_2]: GetCustomerQuerySchema, + }, + resource: AffectedResource.Customer, handler: async (c) => { const ctx = c.get("ctx"); const customerId = c.req.param("customer_id"); - const { env, db, logger, org } = ctx; - const { expand = [] } = c.req.valid("query"); + const { env, db, logger, org, expand } = ctx; // SIDE EFFECT if ( @@ -31,7 +37,7 @@ export const handleGetCustomerV2 = createRoute({ idOrInternalId: customerId, org, env, - expand, + expand: expand as (CusExpand | EntityExpand)[], logger, allowNotFound: false, }); @@ -39,7 +45,6 @@ export const handleGetCustomerV2 = createRoute({ const customer = await getApiCustomer({ ctx, fullCus: fullCus, - expand, }); return c.json(customer); diff --git a/server/src/internal/customers/handlers/handlePostCustomer.ts b/server/src/internal/customers/handlers/handlePostCustomer.ts deleted file mode 100644 index d122ee0b7..000000000 --- a/server/src/internal/customers/handlers/handlePostCustomer.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { CusProductStatus, ErrCode } from "@autumn/shared"; -import RecaseError, { handleRequestError } from "@/utils/errorUtils.js"; -import { parseCusExpand } from "../cusUtils/cusUtils.js"; -import { getCustomerDetails } from "../cusUtils/getCustomerDetails.js"; -import { getOrCreateCustomer } from "../cusUtils/getOrCreateCustomer.js"; - -export const handlePostCustomerRequest = async (req: any, res: any) => { - try { - const logger = req.logger; - const data = req.body; - - const expand = parseCusExpand(req.query.expand); - const { db, org, features } = req; - - if (data.customer_id) { - throw new RecaseError({ - message: - "use the `id` field instead of `customer_id` to specify the new customer's ID", - code: ErrCode.InvalidInputs, - }); - } - - if (data.id === undefined) { - throw new RecaseError({ - message: "`id` field must be either a string or null", - code: ErrCode.InvalidInputs, - }); - } - - if (!data.id && !data.email) { - throw new RecaseError({ - message: "ID or email is required", - code: ErrCode.InvalidRequest, - }); - } - - const customer = await getOrCreateCustomer({ - req, - customerId: data.id, - customerData: data, - inStatuses: [ - CusProductStatus.Active, - CusProductStatus.PastDue, - CusProductStatus.Scheduled, - ], - expand, - - entityId: data.entity_id, - entityData: data.entity_data, - withCache: true, - }); - - const cusDetails = await getCustomerDetails({ - db, - customer, - org, - env: req.env, - params: req.query, - logger, - cusProducts: customer.customer_products, - expand, - features, - apiVersion: req.apiVersion, - }); - - res.status(200).json(cusDetails); - } catch (error: any) { - if ( - error instanceof RecaseError && - error.code === ErrCode.DuplicateCustomerId - ) { - req.logger.warn( - `POST /customers: ${error.message} (org: ${req.org?.slug})`, - ); - res.status(error.statusCode).json({ - message: error.message, - code: error.code, - }); - return; - } - - console.log(`Error: ${error}`); - handleRequestError({ req, error, res, action: "create customer" }); - } -}; diff --git a/server/src/internal/customers/handlers/handleUpdateCustomer.ts b/server/src/internal/customers/handlers/handleUpdateCustomer.ts index b7eb8ec87..1b801d98d 100644 --- a/server/src/internal/customers/handlers/handleUpdateCustomer.ts +++ b/server/src/internal/customers/handlers/handleUpdateCustomer.ts @@ -1,6 +1,7 @@ import { CreateCustomerSchema, ErrCode, ProcessorType } from "@autumn/shared"; import { StatusCodes } from "http-status-codes"; import { createStripeCli } from "@/external/connect/createStripeCli.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { CusService } from "@/internal/customers/CusService.js"; import { FeatureService } from "@/internal/features/FeatureService.js"; import RecaseError from "@/utils/errorUtils.js"; @@ -10,8 +11,8 @@ import type { ExtendedResponse, } from "@/utils/models/Request.js"; import { routeHandler } from "@/utils/routerUtils.js"; +import { getApiCustomer } from "../cusUtils/apiCusUtils/getApiCustomer.js"; import { parseCusExpand } from "../cusUtils/cusUtils.js"; -import { getCustomerDetails } from "../cusUtils/getCustomerDetails.js"; export const handleUpdateCustomer = async (req: any, res: any) => routeHandler({ @@ -134,16 +135,10 @@ export const handleUpdateCustomer = async (req: any, res: any) => }); // res.status(200).json({ customer: updatedCustomer }); - const customerDetails = await getCustomerDetails({ - db, - customer: finalCustomer, - org, - env: req.env, - logger: req.logger, - cusProducts: finalCustomer.customer_products, + const customerDetails = await getApiCustomer({ + ctx: req as AutumnContext, + fullCus: finalCustomer, expand: parseCusExpand(req.query.expand as string), - features, - apiVersion: req.apiVersion, }); res.status(200).json(customerDetails); diff --git a/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts b/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts index e6be7d793..3fe2c7fe3 100644 --- a/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts +++ b/server/src/internal/entities/handlers/handleCreateEntity/handleCreateEntity.ts @@ -4,6 +4,7 @@ import { type Entity, LegacyVersion, } from "@autumn/shared"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import type { ExtendedRequest } from "@/utils/models/Request.js"; import { routeHandler } from "@/utils/routerUtils.js"; import { orgToVersion } from "@/utils/versionUtils/legacyVersionUtils.js"; @@ -91,16 +92,10 @@ export const createEntities = async ({ } const { entities } = await getEntityResponse({ - db, + ctx: req as AutumnContext, entityIds: newEntities.map((e: any) => e.id || e.internal_id), - org, - env, customerId: customer.id || customer.internal_id, withAutumnId, - apiVersion: apiVersion!, - features, - logger, - skipCache: true, }); return entities; diff --git a/server/src/internal/features/internalFeatureRouter.ts b/server/src/internal/features/internalFeatureRouter.ts index 902e7e22c..3b97292a8 100644 --- a/server/src/internal/features/internalFeatureRouter.ts +++ b/server/src/internal/features/internalFeatureRouter.ts @@ -1,8 +1,9 @@ import { CreateFeatureSchema, ErrCode, FeatureType } from "@autumn/shared"; import express, { type Router } from "express"; +import { formatZodError } from "@/errors/formatZodError.js"; import { handleDeleteFeature } from "@/internal/features/handlers/handleDeleteFeature.js"; import { handleUpdateFeature } from "@/internal/features/handlers/handleUpdateFeature.js"; -import RecaseError, { formatZodError } from "@/utils/errorUtils.js"; +import RecaseError from "@/utils/errorUtils.js"; import { generateId } from "@/utils/genUtils.js"; import { FeatureService } from "./FeatureService.js"; import { diff --git a/server/src/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.ts b/server/src/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.ts index 1d5b61ac7..bc665ef46 100644 --- a/server/src/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.ts +++ b/server/src/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.ts @@ -75,7 +75,7 @@ export const isV4Usage = ({ return ( billingType === BillingType.UsageInArrear && - (cusProduct.api_semver === ApiVersion.Beta || + (cusProduct.api_semver === ApiVersion.V1_Beta || notNullish(cusProduct.internal_entity_id)) ); }; diff --git a/server/src/internal/products/productUtils/detectProductVariant.ts b/server/src/internal/products/productUtils/detectProductVariant.ts index 10762a264..51cfaca7f 100644 --- a/server/src/internal/products/productUtils/detectProductVariant.ts +++ b/server/src/internal/products/productUtils/detectProductVariant.ts @@ -1,10 +1,10 @@ -import { BillingInterval, FullProduct } from "@autumn/shared"; -import { generateObject } from "ai"; import { anthropic } from "@ai-sdk/anthropic"; +import { BillingInterval, type FullProduct } from "@autumn/shared"; +import { generateObject } from "ai"; +import type { Logger } from "pino"; import { z } from "zod"; +import type { DrizzleCli } from "@/db/initDrizzle.js"; import { nullish } from "@/utils/genUtils.js"; -import { Logger } from "pino"; -import { DrizzleCli } from "@/db/initDrizzle.js"; import { ProductService } from "../ProductService.js"; const prompt = `Detect whether a given product (called "product_to_detect") is an interval variant of a base product from the list of existing products (called "existing_products"). @@ -35,7 +35,7 @@ export const detectBaseVariant = async ({ logger.info(`Detecting base variant for ${curProduct.id}`); if (!process.env.ANTHROPIC_API_KEY) return; - let existingProducts = (await ProductService.listFull({ + const existingProducts = (await ProductService.listFull({ db, orgId: curProduct.org_id, env: curProduct.env, @@ -43,8 +43,8 @@ export const detectBaseVariant = async ({ })) as FullProduct[]; // if (product.base_variant_id == baseVariantId) { - let curPrices = curProduct.prices; - let intervals = curPrices.map((price) => price.config.interval); + const curPrices = curProduct.prices; + const intervals = curPrices.map((price) => price.config.interval); // 1. Return null if add on if (curProduct.is_add_on) return null; @@ -58,17 +58,17 @@ export const detectBaseVariant = async ({ const filteredExistingProducts = existingProducts.filter( (p) => - p.id != curProduct.id && + p.id !== curProduct.id && nullish(p.base_variant_id) && !p.is_add_on && p.prices.length > 0 && p.prices.every( - (price) => price.config.interval == BillingInterval.Month, + (price) => price.config.interval === BillingInterval.Month, ) && - p.group == curProduct.group, + p.group === curProduct.group, ); - if (filteredExistingProducts.length == 0) { + if (filteredExistingProducts.length === 0) { logger.info(`No base product to search for`); return null; } @@ -95,13 +95,13 @@ export const detectBaseVariant = async ({ `; - let { object } = await generateObject({ + const { object } = await generateObject({ model: anthropic("claude-3-5-haiku-latest"), schema: z.object({ base_variant_id: z.string().nullable() }), prompt: `${prompt}\n\n${variables}`, }); - let baseVariantId = object.base_variant_id; + const baseVariantId = object.base_variant_id; logger.info( `llm response for base variant of ${curProduct.id}: ${baseVariantId}`, diff --git a/server/src/internal/products/productUtils/productResponseUtils/getPlanResponse.ts b/server/src/internal/products/productUtils/productResponseUtils/getPlanResponse.ts index 71087aa26..a187210e6 100644 --- a/server/src/internal/products/productUtils/productResponseUtils/getPlanResponse.ts +++ b/server/src/internal/products/productUtils/productResponseUtils/getPlanResponse.ts @@ -4,12 +4,12 @@ import { type ApiPlan, ApiPlanSchema, AttachScenario, - type BillingInterval, type Feature, type FeatureOptions, type FullCustomer, type FullProduct, itemsToPlanFeatures, + itemToBillingInterval, productV2ToBasePrice, productV2ToFeatureItems, } from "@autumn/shared"; @@ -114,62 +114,13 @@ export const getPlanResponse = async ({ attachScenario, }); - console.log( - `New feature:\n ${JSON.stringify( - { - // Basic fields - id: product.id, - name: product.name || "", - description: product.description, // Products don't have descriptions - group: product.group, - version: product.version, - - // Boolean flags - add_on: product.is_add_on, - default: product.is_default, - - // Price field (required in Plan schema) - price: basePrice - ? { - amount: basePrice.amount, - interval: basePrice.interval as unknown as BillingInterval, - } - : { - amount: 0, - interval: "month" as BillingInterval, - }, - - // Features array - features: planFeatures, - - // Free trial - free_trial: freeTrial, - - // Metadata fields - created_at: product.created_at, - env: product.env, - archived: product.archived, - base_variant_id: product.base_variant_id, - - // Customer context (optional) - // Uncomment when ready to add customer context - // customer_context: { - // trial_available: notNullish(freeTrial), - // scenario: attachScenario, - // }, - }, - null, - 4, - )}`, - ); - // 9. Build Plan response return ApiPlanSchema.parse({ // Basic fields id: product.id, - name: product.name || "", - description: product.description, // Products don't have descriptions - group: product.group, + name: product.name || null, + description: product.description || null, // Products don't have descriptions + group: product.group || null, version: product.version, // Boolean flags @@ -180,12 +131,11 @@ export const getPlanResponse = async ({ price: basePrice ? { amount: basePrice.amount, - interval: basePrice.interval as BillingInterval, + interval: itemToBillingInterval({ item: basePrice }), + interval_count: + basePrice.intervalCount !== 1 ? basePrice.intervalCount : undefined, } - : { - amount: 0, - interval: "month" as BillingInterval, - }, + : null, // Features array features: planFeatures, diff --git a/server/src/internal/products/productV2Utils.ts b/server/src/internal/products/productV2Utils.ts index 9d38d3c6d..b5a1baacd 100644 --- a/server/src/internal/products/productV2Utils.ts +++ b/server/src/internal/products/productV2Utils.ts @@ -43,7 +43,7 @@ export const mapToProductItems = ({ } for (const item of items) { - const feature = features.find((f) => f.id == item.feature_id); + const feature = features.find((f) => f.id === item.feature_id); if (feature) { item.feature_type = getItemFeatureType({ item, features }); } @@ -84,7 +84,7 @@ export const mapToProductV2 = ({ const productV2: ProductV2 = { internal_id: product.internal_id, - + env: product.env, id: product.id, name: product.name, is_add_on: product.is_add_on, diff --git a/server/src/queue/workersInit.ts b/server/src/queue/workersInit.ts index ac33276f2..8fd4aefc8 100644 --- a/server/src/queue/workersInit.ts +++ b/server/src/queue/workersInit.ts @@ -12,6 +12,7 @@ import { runTriggerCheckoutReward } from "@/internal/rewards/triggerCheckoutRewa import { runUpdateBalanceTask } from "@/trigger/updateBalanceTask.js"; import { runUpdateUsageTask } from "@/trigger/updateUsageTask.js"; import { generateId } from "@/utils/genUtils.js"; +import { createWorkerAutumnContext } from "@/utils/workerUtils/createAutumnContext.js"; import { JobName } from "./JobName.js"; import { acquireLock, getRedisConnection, releaseLock } from "./lockUtils.js"; import { QueueManager } from "./QueueManager.js"; @@ -79,11 +80,18 @@ const initWorker = ({ } if (actionHandlers.includes(job.name as JobName)) { + const ctx = await createWorkerAutumnContext({ + db, + orgId: job.data.orgId, + env: job.data.env, + logger: workerLogger, + workerId: id.toString(), + }); + await runActionHandlerTask({ queue, job, - logger: workerLogger, - db, + ctx, useBackup, }); return; diff --git a/server/src/trigger/handleThresholdReached.ts b/server/src/trigger/handleThresholdReached.ts index 1d72d36c3..b68c11cd3 100644 --- a/server/src/trigger/handleThresholdReached.ts +++ b/server/src/trigger/handleThresholdReached.ts @@ -1,5 +1,6 @@ import { type AppEnv, + AuthType, createdAtToVersion, type Feature, type FullCusEntWithFullCusProduct, @@ -10,10 +11,12 @@ import { } from "@autumn/shared"; import type { DrizzleCli } from "@/db/initDrizzle.js"; import { sendSvixEvent } from "@/external/svix/svixHelpers.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; import { getSingleEntityResponse } from "@/internal/api/entities/getEntityUtils.js"; import { getV2CheckResponse } from "@/internal/api/entitled/checkUtils/getV2CheckResponse.js"; -import { getCustomerDetails } from "@/internal/customers/cusUtils/getCustomerDetails.js"; +import { getApiCustomer } from "@/internal/customers/cusUtils/apiCusUtils/getApiCustomer.js"; import { toApiFeature } from "@/internal/features/utils/mapFeatureUtils.js"; +import { generateId } from "@/utils/genUtils.js"; export const mergeNewCusEntsIntoCusProducts = ({ cusProducts, @@ -39,45 +42,27 @@ export const mergeNewCusEntsIntoCusProducts = ({ }; export const sendSvixThresholdReachedEvent = async ({ - db, - org, - env, - features, - logger, + ctx, feature, fullCus, thresholdType, }: { - db: DrizzleCli; - org: Organization; - env: AppEnv; - features: Feature[]; - logger: any; + ctx: AutumnContext; feature: Feature; fullCus: FullCustomer; thresholdType: "limit_reached" | "allowance_used"; }) => { - const apiVersion = createdAtToVersion({ - createdAt: org.created_at || undefined, - }); + const { org, env, logger } = ctx; - const cusDetails = await getCustomerDetails({ - db, - customer: fullCus, - org, - env, - features, - logger, - cusProducts: fullCus.customer_products, + const cusDetails = await getApiCustomer({ + ctx, + fullCus, expand: [], - apiVersion, }); if (fullCus.entity) { await getSingleEntityResponse({ - org, - env, - features, + ctx, fullCus, entity: fullCus.entity, entityId: fullCus.entity.id, @@ -100,31 +85,20 @@ export const sendSvixThresholdReachedEvent = async ({ }; export const handleAllowanceUsed = async ({ - db, - org, - env, - features, - logger, + ctx, cusEnts, newCusEnts, feature, fullCus, }: { - db: DrizzleCli; - org: Organization; - env: AppEnv; + ctx: AutumnContext; cusEnts: FullCusEntWithFullCusProduct[]; newCusEnts: FullCusEntWithFullCusProduct[]; feature: Feature; fullCus: FullCustomer; - features: Feature[]; - logger: any; }) => { - const apiVersion = createdAtToVersion({ - createdAt: org.created_at || undefined, - }); + const { org, env, apiVersion } = ctx; - // Allowance used... // Make sure overage allowed is false const oldCusEnts = structuredClone(cusEnts); for (const cusEnt of oldCusEnts) { @@ -166,13 +140,9 @@ export const handleAllowanceUsed = async ({ if (prevCheckResponse.allowed === true && v2CheckResponse.allowed === false) { await sendSvixThresholdReachedEvent({ - db, - org, - env, - features, - logger, - feature, + ctx, fullCus, + feature, thresholdType: "allowance_used", }); } @@ -200,6 +170,21 @@ export const handleThresholdReached = async ({ features: Feature[]; logger: any; }) => { + const ctx: AutumnContext = { + db, + org, + env, + features, + logger, + apiVersion: createdAtToVersion({ + createdAt: org.created_at || undefined, + }), + id: generateId("local_req"), + isPublic: false, + authType: AuthType.Unknown, + timestamp: Date.now(), + }; + try { const apiVersion = createdAtToVersion({ createdAt: org.created_at || undefined, @@ -236,26 +221,18 @@ export const handleThresholdReached = async ({ prevCheckResponse.allowed === true && v2CheckResponse.allowed === false ) { - const cusDetails = await getCustomerDetails({ - db, - customer: fullCus, - org, - env, - features, - logger, - cusProducts: newCusProducts, + const cusDetails = await getApiCustomer({ + ctx, + fullCus, expand: [], - apiVersion, }); if (fullCus.entity) { await getSingleEntityResponse({ - org, - env, - features, - entity: fullCus.entity, - fullCus, + ctx, entityId: fullCus.entity.id, + fullCus, + entity: fullCus.entity, }); } @@ -276,11 +253,7 @@ export const handleThresholdReached = async ({ return; } await handleAllowanceUsed({ - db, - org, - env, - features, - logger, + ctx, cusEnts, newCusEnts, feature, diff --git a/server/src/utils/errorUtils.ts b/server/src/utils/errorUtils.ts index b282b3240..244fc6c02 100644 --- a/server/src/utils/errorUtils.ts +++ b/server/src/utils/errorUtils.ts @@ -2,7 +2,8 @@ import { ErrCode } from "@autumn/shared"; import chalk from "chalk"; import { StatusCodes } from "http-status-codes"; import Stripe from "stripe"; -import { ZodError, type ZodIssue } from "zod/v4"; +import { ZodError } from "zod/v4"; +import { formatZodError } from "../errors/formatZodError.js"; export const isPaymentDeclined = (error: any) => { return ( @@ -46,51 +47,6 @@ export default class RecaseError extends Error { } } -export function formatZodError(error: ZodError): string { - const formatMessage = (issue: ZodIssue): string => { - const path = issue.path.length ? issue.path.join(".") : "input"; - - // Clean up common Zod error messages - let message = issue.message; - - // Handle common patterns and make them more user-friendly - if ( - message.includes("Too small") && - message.includes("expected string to have >=1 characters") - ) { - message = "cannot be empty"; - } else if (message.includes("Invalid string: must match pattern")) { - // Extract the pattern and make it more readable - if (message.includes("/^[a-zA-Z0-9_-]+$/")) { - message = - "must contain only letters, numbers, underscores, and hyphens"; - } else { - message = "has invalid format"; - } - } else if (message.includes("Invalid input: expected string, received")) { - const receivedType = message.split("received ")[1]; - message = `must be a string (received ${receivedType})`; - } else if (message.includes("Invalid input: expected number, received")) { - const receivedType = message.split("received ")[1]; - message = `must be a number (received ${receivedType})`; - } else if (message.includes("Invalid input: expected boolean, received")) { - const receivedType = message.split("received ")[1]; - message = `must be a boolean (received ${receivedType})`; - } - - return `${path}: ${message}`; - }; - - const formattedIssues = error.issues.map(formatMessage); - - // If there are multiple issues, format them nicely - if (formattedIssues.length === 1) { - return formattedIssues[0]; - } else { - return `[Validation Errors] ${formattedIssues.join("; ")}`; - } -} - export const handleRequestError = ({ error, req, diff --git a/server/src/utils/routerUtils.ts b/server/src/utils/routerUtils.ts index 27c3b4b40..934a27032 100644 --- a/server/src/utils/routerUtils.ts +++ b/server/src/utils/routerUtils.ts @@ -3,11 +3,9 @@ import { StatusCodes } from "http-status-codes"; import qs from "qs"; import Stripe from "stripe"; import { ZodAny, ZodError, ZodObject } from "zod"; +import { formatZodError } from "@/errors/formatZodError.js"; import { withSpan as withSpanTracer } from "@/internal/analytics/tracer/spanUtils.js"; -import RecaseError, { - formatZodError, - handleRequestError, -} from "./errorUtils.js"; +import RecaseError, { handleRequestError } from "./errorUtils.js"; import type { ExtendedRequest } from "./models/Request.js"; /** diff --git a/server/src/utils/workerUtils/createAutumnContext.ts b/server/src/utils/workerUtils/createAutumnContext.ts new file mode 100644 index 000000000..41a622fcb --- /dev/null +++ b/server/src/utils/workerUtils/createAutumnContext.ts @@ -0,0 +1,64 @@ +import { + type AppEnv, + AuthType, + createdAtToVersion, + ErrCode, + RecaseError, +} from "@autumn/shared"; + +import type { DrizzleCli } from "@/db/initDrizzle.js"; +import type { Logger } from "@/external/logtail/logtailUtils.js"; +import type { AutumnContext } from "@/honoUtils/HonoEnv.js"; +import { OrgService } from "@/internal/orgs/OrgService.js"; + +export const createWorkerAutumnContext = async ({ + db, + orgId, + env, + logger, + workerId, +}: { + db: DrizzleCli; + orgId: string; + env: AppEnv; + logger: Logger; + workerId: string; +}): Promise => { + // 1. Get org with features + const data = await OrgService.getWithFeatures({ + db, + orgId, + env, + }); + + if (!data) { + logger.warn(`Org ${orgId} not found in DB`); + throw new RecaseError({ + message: "Org not found", + code: ErrCode.OrgNotFound, + statusCode: 500, + }); + } + + const { org, features } = data; + + const apiVersion = createdAtToVersion({ + createdAt: org.created_at || Date.now(), + }); + + return { + org, + env, + features, + + db, + logger, + expand: [], + + id: workerId, + isPublic: false, + authType: AuthType.Unknown, + apiVersion, + timestamp: Date.now(), + } satisfies AutumnContext; +}; diff --git a/server/src/utils/workerUtils/jobTypes/HandleCustomerCreatedData.ts b/server/src/utils/workerUtils/jobTypes/HandleCustomerCreatedData.ts new file mode 100644 index 000000000..a5a93900e --- /dev/null +++ b/server/src/utils/workerUtils/jobTypes/HandleCustomerCreatedData.ts @@ -0,0 +1,9 @@ +import type { AppEnv } from "@autumn/shared"; +import type { ExtendedRequest } from "@/utils/models/Request.js"; + +export interface HandleCustomerCreatedData { + req: Partial; + orgId: string; + env: AppEnv; + internalCustomerId: string; +} diff --git a/server/tests/merged/mergeUtils.test.ts b/server/tests/merged/mergeUtils.test.ts index c76244523..0b59255a5 100644 --- a/server/tests/merged/mergeUtils.test.ts +++ b/server/tests/merged/mergeUtils.test.ts @@ -81,7 +81,7 @@ export const expectSubToBeCorrect = async ({ existingUsage: 0, withEntity: true, isCheckout: false, - apiVersion: ApiVersion.Beta, + apiVersion: ApiVersion.V1_Beta, }); const lineItem: any = res?.lineItem; diff --git a/shared/api/core/checkModels.ts b/shared/api/core/checkModels.ts index ec496cd8e..4ab10f35a 100644 --- a/shared/api/core/checkModels.ts +++ b/shared/api/core/checkModels.ts @@ -2,7 +2,7 @@ import { EntityDataSchema } from "@api/common/entityData.js"; import { ApiProductSchema } from "@api/products/previousVersions/apiProduct.js"; import { z } from "zod/v4"; import { CustomerDataSchema } from "../common/customerData.js"; -import { CoreCusFeatureSchema } from "../customers/cusFeatures/apiCusFeature.js"; +import { CoreCusFeatureSchema } from "../customers/cusFeatures/previousVersions/apiCusFeatureV3.js"; // Check Feature Enums export const CheckFeatureScenarioSchema = z.enum([ diff --git a/shared/api/customers/apiCustomer.ts b/shared/api/customers/apiCustomer.ts index 6ef7f6a1c..79290c847 100644 --- a/shared/api/customers/apiCustomer.ts +++ b/shared/api/customers/apiCustomer.ts @@ -1,12 +1,12 @@ -import { ApiCusReferralSchema } from "@api/customers/components/apiCusReferral.js"; -import { ApiCusFeatureSchema } from "@api/customers/cusFeatures/apiCusFeature.js"; import { ApiBaseEntitySchema } from "@api/entities/apiEntity.js"; import { ApiCusRewardsSchema } from "@api/others/apiDiscount.js"; import { ApiInvoiceSchema } from "@api/others/apiInvoice.js"; import { AppEnv } from "@models/genModels/genEnums.js"; import { z } from "zod/v4"; +import { ApiCusReferralSchema } from "./components/apiCusReferral.js"; import { ApiCusUpcomingInvoiceSchema } from "./components/apiCusUpcomingInvoice.js"; -import { ApiCusProductSchema } from "./cusProducts/apiCusProduct.js"; +import { ApiCusFeatureSchema } from "./cusFeatures/apiCusFeature.js"; +import { ApiCusPlanSchema } from "./cusPlans/apiCusPlan.js"; export const ApiTrialsUsedSchema = z.object({ product_id: z.string(), @@ -25,47 +25,20 @@ export const ApiCusExpandSchema = z.object({ }); export const ApiCustomerSchema = z.object({ - // Internal fields - id: z.string().nullable().meta({ - description: "Your internal ID for the customer", - example: "cus_123", - }), - created_at: z.number().meta({ - description: "Timestamp of customer creation in milliseconds since epoch", - example: 1717000000, - }), - name: z.string().nullable().meta({ - description: "Customer’s name", - example: "John Doe", - }), - email: z.string().nullable().meta({ - description: "Customer’s email address", - example: "john@doe.com", - }), - fingerprint: z.string().nullable().meta({ - description: - "Unique identifier (eg. serial number) to detect duplicate customers and prevent key leaks", - example: "fp_9184And92839123hda", - }), - stripe_id: z.string().nullable().default(null).meta({ - description: "Stripe customer ID", - example: "cus_J8A5c31A8tlpwN", - }), - env: z.enum(AppEnv).meta({ - description: "Environment the customer is in", - example: "production", - }), - metadata: z.record(z.any(), z.any()).default({}), - products: z.array(ApiCusProductSchema).meta({ - description: "List of products the customer has access to", - example: [], - }), - features: z.record(z.string(), ApiCusFeatureSchema).meta({ - description: "List of features the customer has access to", - example: {}, - }), + id: z.string(), + name: z.string().nullable(), + email: z.string().nullable(), + created_at: z.number(), + fingerprint: z.string().nullable(), + stripe_id: z.string().nullable(), + env: z.enum(AppEnv), + metadata: z.record(z.any(), z.any()), + + plans: z.array(ApiCusPlanSchema), + + features: z.record(z.string(), ApiCusFeatureSchema), ...ApiCusExpandSchema.shape, }); export type ApiCustomer = z.infer; -export type ApiCustomerExpand = z.infer; +export type ApiCusExpand = z.infer; diff --git a/shared/api/customers/changes/V0.2_CustomerChange.ts b/shared/api/customers/changes/V0.2_CustomerChange.ts index 24d8e84df..b07b980d1 100644 --- a/shared/api/customers/changes/V0.2_CustomerChange.ts +++ b/shared/api/customers/changes/V0.2_CustomerChange.ts @@ -9,7 +9,7 @@ import { ApiCusFeatureV1Schema, } from "../cusFeatures/previousVersions/apiCusFeatureV1.js"; import type { ApiCusFeatureV2 } from "../cusFeatures/previousVersions/apiCusFeatureV2.js"; -import { transformCusProductV2ToV1 } from "../cusProducts/changes/V0_2_CusProductChange.js"; +import { transformCusProductV2ToV1 } from "../cusPlans/changes/V0.2_CusProductChange.js"; import { type CustomerLegacyData, CustomerLegacyDataSchema, diff --git a/shared/api/customers/changes/V1.1_FeaturesArrayToObject.ts b/shared/api/customers/changes/V1.1_FeaturesArrayToObject.ts index 12bdf8497..4dbf600b2 100644 --- a/shared/api/customers/changes/V1.1_FeaturesArrayToObject.ts +++ b/shared/api/customers/changes/V1.1_FeaturesArrayToObject.ts @@ -1,4 +1,3 @@ -import { ApiCustomerSchema } from "@api/customers/apiCustomer.js"; import { ApiFeatureType } from "@api/features/apiFeature.js"; import { ApiVersion } from "@api/versionUtils/ApiVersion.js"; import { @@ -10,6 +9,7 @@ import { type ApiCusFeatureV2, ApiCusFeatureV2Schema, } from "../cusFeatures/previousVersions/apiCusFeatureV2.js"; +import { ApiCustomerV3Schema } from "../previousVersions/apiCustomerV3.js"; /** * V1_2: Features changed from array to object AND merged multi-interval features @@ -24,10 +24,10 @@ import { */ // V1_2+ customer schema (features as object with breakdown) -const V1_2_CustomerSchema = ApiCustomerSchema; +const V1_2_CustomerSchema = ApiCustomerV3Schema; // V1_1 customer schema (features as array without breakdown) -const V1_1_CustomerSchema = ApiCustomerSchema.extend({ +const V1_1_CustomerSchema = ApiCustomerV3Schema.extend({ features: z.array(ApiCusFeatureV2Schema), }); diff --git a/shared/api/customers/changes/V1.2_CustomerChange.ts b/shared/api/customers/changes/V1.2_CustomerChange.ts new file mode 100644 index 000000000..1ea4510c8 --- /dev/null +++ b/shared/api/customers/changes/V1.2_CustomerChange.ts @@ -0,0 +1,79 @@ +import { ApiVersion } from "@api/versionUtils/ApiVersion.js"; +import { + AffectedResource, + defineVersionChange, +} from "@api/versionUtils/versionChangeUtils/VersionChange.js"; +import type { z } from "zod/v4"; +import { ApiCustomerSchema } from "../apiCustomer.js"; +import { transformCusFeatureToV3 } from "../cusFeatures/changes/V1.2_CusFeatureChange.js"; +import type { ApiCusFeatureV3 } from "../cusFeatures/previousVersions/apiCusFeatureV3.js"; +import { transformCusPlanToProductV3 } from "../cusPlans/changes/V1.2_CusPlanChange.js"; +import type { ApiCusProductV3 } from "../cusPlans/previousVersions/apiCusProductV3.js"; +import { CustomerLegacyDataSchema } from "../customerLegacyData.js"; +import { ApiCustomerV3Schema } from "../previousVersions/apiCustomerV3.js"; +/** + * V1_2_CustomerChange: Transforms customer response TO V1_2 format + * + * Applied when: targetVersion <= V1_2 + * + * Breaking changes introduced in V2.0: + * + * 1. Products renamed to Plans: + * - V2.0+: "products" field contains ApiCusPlan objects + * - V1.2: "products" field contains ApiCusProductV3 objects + * + * 2. Simplified feature schema: + * - V2.0+: Minimal fields with optional feature object + * - V1.2: Verbose fields with all metadata + * + * 3. Features remain as record (no change from V1.1): + * - Both V2.0 and V1.2: Record + * + * Input: ApiCustomer (V2.0+ format) + * Output: ApiCustomerV3 (V1.2 format) + */ + +export const V1_2_CustomerChange = defineVersionChange({ + newVersion: ApiVersion.V2, + oldVersion: ApiVersion.V1_Beta, + description: [ + "Products renamed to plans in SDK", + "Simplified feature and plan schemas", + "Added optional nested objects for expanded data", + ], + affectedResources: [AffectedResource.Customer], + newSchema: ApiCustomerSchema, + oldSchema: ApiCustomerV3Schema, + legacyDataSchema: CustomerLegacyDataSchema, + + // Response: V2.0 → V1.2 + transformResponse: ({ input, legacyData }) => { + // Step 1: Transform plans V2.0 → V1.2 (products) + const v3CusProducts: ApiCusProductV3[] = input.plans.map((plan) => + transformCusPlanToProductV3({ + input: plan, + legacyData: legacyData?.cusProductLegacyData[plan.plan_id], + }), + ); + + // Step 2: Transform features V2.0 → V1.2 + const v3_features: Record = {}; + for (const [featureId, feature] of Object.entries(input.features)) { + v3_features[featureId] = transformCusFeatureToV3({ input: feature }); + } + + // Step 3: Return V1.2 customer format + return { + id: input.id, + name: input.name, + email: input.email, + created_at: input.created_at, + fingerprint: input.fingerprint, + stripe_id: input.stripe_id, + env: input.env, + metadata: input.metadata, + products: v3CusProducts, + features: v3_features, + } satisfies z.infer; + }, +}); diff --git a/shared/api/customers/cusFeatures/apiCusFeature.ts b/shared/api/customers/cusFeatures/apiCusFeature.ts index e925d2183..1ac9b2162 100644 --- a/shared/api/customers/cusFeatures/apiCusFeature.ts +++ b/shared/api/customers/cusFeatures/apiCusFeature.ts @@ -1,138 +1,56 @@ -import { ApiFeatureType } from "@api/features/apiFeature.js"; -import { EntInterval } from "@models/productModels/entModels/entEnums.js"; +import { ApiFeatureSchema } from "@api/features/apiFeature.js"; +import { ResetInterval } from "@api/products/planEnums.js"; import { z } from "zod/v4"; export const ApiCusRolloverSchema = z.object({ - balance: z.number().meta({ - description: "The remaining balance amount that has rolled over", - example: 100, - }), - expires_at: z.number().meta({ - description: "Timestamp when the rollover balance expires", - example: 1759247877000, - }), + balance: z.number(), + expires_at: z.number(), }); -// Version 3 of cus feature response export const ApiCusFeatureBreakdownSchema = z.object({ - interval: z.enum(EntInterval).meta({ - description: "The billing interval for this feature breakdown", - example: "month", - }), - interval_count: z.number().nullish().meta({ - description: "The number of intervals between resets", - example: 1, - }), - balance: z.number().nullish().meta({ - description: "The remaining balance for this interval", - example: 500, - }), - usage: z.number().nullish().meta({ - description: "The current usage amount", - example: 250, - }), - included_usage: z.number().nullish().meta({ - description: "The amount of usage included in this interval", - example: 1000, - }), - next_reset_at: z.number().nullish().meta({ - description: "Timestamp when the usage resets", - example: 1759247877000, - }), - usage_limit: z.number().nullish().meta({ - description: "The maximum usage allowed", - example: 1000, - }), - rollovers: z.array(ApiCusRolloverSchema).nullish().meta({ - description: "Array of rollover balances from previous periods", - example: [{ balance: 100, expires_at: 1759247877000 }], - }), + reset: z + .object({ + interval: z.enum(ResetInterval), + interval_count: z.number().optional(), + when_enabled: z.boolean().optional(), + }) + .optional(), + + starting_balance: z.number(), + balance: z.number(), + usage: z.number(), + resets_at: z.number().nullable(), + + max_purchase: z.number().optional(), }); -export const CoreCusFeatureSchema = z.object({ - interval: z.enum(EntInterval).or(z.literal("multiple")).nullish().meta({ - description: "The billing interval or 'multiple' if the feature has multiple intervals", - example: "month", - }), - interval_count: z.number().nullish().meta({ - description: "The number of intervals between resets", - example: 1, - }), - unlimited: z.boolean().nullish().meta({ - description: "Whether the feature has unlimited usage", - example: false, - }), - balance: z.number().nullish().meta({ - description: "The remaining balance for this feature", - example: 500, - }), - usage: z.number().nullish().meta({ - description: "The current usage amount", - example: 250, - }), - included_usage: z.number().nullish().meta({ - description: "The amount of usage included", - example: 1000, - }), - next_reset_at: z.number().nullish().meta({ - description: "Timestamp when the usage resets", - example: 1759247877000, - }), - overage_allowed: z.boolean().nullish().meta({ - description: "Whether overage usage beyond the limit is allowed", - example: true, - }), +export const ApiCusFeatureSchema = z.object({ + feature_id: z.string(), + feature: ApiFeatureSchema.optional(), - breakdown: z.array(ApiCusFeatureBreakdownSchema).nullish().meta({ - description: "Detailed breakdown by interval for features with multiple intervals", - example: [{ interval: "month", interval_count: 1, balance: 500, usage: 250 }], - }), - credit_schema: z - .array( - z.object({ - feature_id: z.string().meta({ - description: "The ID of the feature that credits are applied to", - example: "feature_123", - }), - credit_amount: z.number().meta({ - description: "The amount of credits applied per usage", - example: 10, - }), - }), - ) - .nullish() - .meta({ - description: "Credit conversion schema for credit system features", - example: [{ feature_id: "feature_123", credit_amount: 10 }], - }), + unlimited: z.boolean(), - usage_limit: z.number().nullish().meta({ - description: "The maximum usage allowed", - example: 1000, - }), - rollovers: z.array(ApiCusRolloverSchema).nullish().meta({ - description: "Array of rollover balances from previous periods", - example: [{ balance: 100, expires_at: 1759247877000 }], - }), + granted_balance: z.number(), // 0 + purchased_balance: z.number(), // 3 + current_balance: z.number(), // 1 + usage: z.number(), // 2 + + resets_at: z.number().nullable(), + + max_purchase: z.number().optional(), + pay_per_use: z.boolean().optional(), + + reset: z + .object({ + interval: z.enum(ResetInterval).nullable(), + interval_count: z.number().optional(), + }) + .optional(), + + breakdown: z.array(ApiCusFeatureBreakdownSchema).nullish(), + rollovers: z.array(ApiCusRolloverSchema).nullish(), }); -export const ApiCusFeatureSchema = z - .object({ - id: z.string().meta({ - description: "The unique identifier of the feature", - example: "feature_123", - }), - type: z.enum(ApiFeatureType).meta({ - description: "The type of the feature", - example: "single_use", - }), - name: z.string().nullish().meta({ - description: "The name of the feature", - example: "API Calls", - }), - }) - .extend(CoreCusFeatureSchema.shape); - export type ApiCusFeature = z.infer; export type ApiCusRollover = z.infer; export type ApiCusFeatureBreakdown = z.infer< diff --git a/shared/api/customers/cusFeatures/changes/V1.2_CusFeatureChange.ts b/shared/api/customers/cusFeatures/changes/V1.2_CusFeatureChange.ts new file mode 100644 index 000000000..39b138d8e --- /dev/null +++ b/shared/api/customers/cusFeatures/changes/V1.2_CusFeatureChange.ts @@ -0,0 +1,153 @@ +import { ApiFeatureType } from "@api/features/apiFeature.js"; +import { ApiVersion } from "@api/versionUtils/ApiVersion.js"; +import { + AffectedResource, + defineVersionChange, +} from "@api/versionUtils/versionChangeUtils/VersionChange.js"; +import { resetIntvToEntIntv } from "@utils/planFeatureUtils/planFeatureIntervals.js"; +import { nullish } from "@utils/utils.js"; +import { Decimal } from "decimal.js"; +import type { z } from "zod/v4"; +import { ApiCusFeatureSchema } from "../apiCusFeature.js"; +import { + type ApiCusFeatureV3Breakdown, + ApiCusFeatureV3Schema, +} from "../previousVersions/apiCusFeatureV3.js"; +// overage_allowed: z.boolean().nullish().meta({ +// description: "Whether overage usage beyond the limit is allowed", +// example: true, +// }), + +/** + * Transform feature from V2.0 format to V1.2 format + * Exported so it can be reused in other transformations (e.g., V1_2_CustomerChange) + */ +export function transformCusFeatureToV3({ + input, +}: { + input: z.infer; +}): z.infer { + const toUsageLimit = ({ + maxPurchase, + startingBalance, + }: { + maxPurchase?: number; + startingBalance: number; + }) => { + return maxPurchase && startingBalance + ? new Decimal(maxPurchase).add(startingBalance).toNumber() + : undefined; + }; + + const toV3Type = ({ type }: { type: ApiFeatureType }) => { + if (type === ApiFeatureType.Boolean) { + return ApiFeatureType.Static; + } else return type; + }; + + const itemInterval = + input.reset_interval === null + ? "multiple" + : resetIntvToEntIntv({ resetIntv: input.reset_interval }); + + const usageLimit = toUsageLimit({ + maxPurchase: input.max_purchase, + startingBalance: input.starting_balance, + }); + + const overageAllowed = + Boolean(input.pay_per_use) && nullish(input.max_purchase); + + let newBreakdown: ApiCusFeatureV3Breakdown[] | undefined; + if (input.breakdown && input.breakdown.length > 0) { + newBreakdown = input.breakdown.map((breakdown) => { + const interval = resetIntvToEntIntv({ + resetIntv: breakdown.reset_interval, + }); + + const usageLimit = toUsageLimit({ + maxPurchase: breakdown.max_purchase, + startingBalance: breakdown.starting_balance, + }); + + return { + interval, + interval_count: breakdown.reset_interval_count || 1, + + balance: breakdown.balance, + usage: breakdown.usage, + included_usage: breakdown.starting_balance, + next_reset_at: breakdown.resets_at, + usage_limit: usageLimit, + } satisfies ApiCusFeatureV3Breakdown; + }); + } + + const v3Type = toV3Type({ + type: input.feature?.type ?? ApiFeatureType.SingleUsage, + }); + + const omitInterval = v3Type === ApiFeatureType.Static || input.unlimited; + + return { + id: input.feature_id, + type: v3Type, + + name: input.feature?.name ?? null, + unlimited: input.unlimited, + + included_usage: input.starting_balance, + balance: input.balance, + usage: input.usage, + next_reset_at: input.resets_at, + + interval: omitInterval ? undefined : itemInterval, + interval_count: omitInterval + ? undefined + : itemInterval === "multiple" + ? null + : input.reset_interval_count || 1, + + overage_allowed: overageAllowed, + + credit_schema: input.feature?.credit_schema + ? input.feature.credit_schema.map((credit) => ({ + feature_id: credit.metered_feature_id, + credit_amount: credit.credit_cost, + })) + : undefined, + + breakdown: newBreakdown, + usage_limit: usageLimit, + rollovers: input.rollovers, + }; +} + +/** + * V1_2_CusFeatureChange: Transforms customer feature response TO V1_2 format + * + * Applied when: targetVersion <= V1_2 + * + * Breaking changes introduced in V2.0: + * + * 1. Simplified schema with minimal required fields + * 2. Optional feature object for expanded data + * 3. Removed verbose metadata and display fields + * + * Input: ApiCusFeature (V2.0+ format) + * Output: ApiCusFeatureV3 (V1.2 format) + */ +export const V1_2_CusFeatureChange = defineVersionChange({ + newVersion: ApiVersion.V2, + oldVersion: ApiVersion.V1_2, + description: [ + "Simplified customer feature schema", + "Added optional feature object", + "Removed verbose fields", + ], + affectedResources: [AffectedResource.Customer], + newSchema: ApiCusFeatureSchema, + oldSchema: ApiCusFeatureV3Schema, + + transformResponse: transformCusFeatureToV3, +}); diff --git a/shared/api/customers/cusFeatures/previousVersions/apiCusFeatureV3.ts b/shared/api/customers/cusFeatures/previousVersions/apiCusFeatureV3.ts new file mode 100644 index 000000000..4271ace2d --- /dev/null +++ b/shared/api/customers/cusFeatures/previousVersions/apiCusFeatureV3.ts @@ -0,0 +1,155 @@ +import { ApiFeatureType } from "@api/features/apiFeature.js"; +import { EntInterval } from "@models/productModels/entModels/entEnums.js"; +import { z } from "zod/v4"; + +export const ApiCusFeatureV3RolloverSchema = z.object({ + balance: z.number().meta({ + description: "The remaining balance amount that has rolled over", + example: 100, + }), + expires_at: z.number().meta({ + description: "Timestamp when the rollover balance expires", + example: 1759247877000, + }), +}); + +// Version 3 of cus feature response +export const ApiCusFeatureV3BreakdownSchema = z.object({ + interval: z.enum(EntInterval).meta({ + description: "The billing interval for this feature breakdown", + example: "month", + }), + interval_count: z.number().nullish().meta({ + description: "The number of intervals between resets", + example: 1, + }), + balance: z.number().nullish().meta({ + description: "The remaining balance for this interval", + example: 500, + }), + usage: z.number().nullish().meta({ + description: "The current usage amount", + example: 250, + }), + included_usage: z.number().nullish().meta({ + description: "The amount of usage included in this interval", + example: 1000, + }), + next_reset_at: z.number().nullish().meta({ + description: "Timestamp when the usage resets", + example: 1759247877000, + }), + usage_limit: z.number().nullish().meta({ + description: "The maximum usage allowed", + example: 1000, + }), + rollovers: z + .array(ApiCusFeatureV3RolloverSchema) + .nullish() + .meta({ + description: "Array of rollover balances from previous periods", + example: [{ balance: 100, expires_at: 1759247877000 }], + }), +}); + +export const CoreCusFeatureSchema = z.object({ + interval: z.enum(EntInterval).or(z.literal("multiple")).nullish().meta({ + description: + "The billing interval or 'multiple' if the feature has multiple intervals", + example: "month", + }), + interval_count: z.number().nullish().meta({ + description: "The number of intervals between resets", + example: 1, + }), + unlimited: z.boolean().nullish().meta({ + description: "Whether the feature has unlimited usage", + example: false, + }), + balance: z.number().nullish().meta({ + description: "The remaining balance for this feature", + example: 500, + }), + usage: z.number().nullish().meta({ + description: "The current usage amount", + example: 250, + }), + included_usage: z.number().nullish().meta({ + description: "The amount of usage included", + example: 1000, + }), + next_reset_at: z.number().nullish().meta({ + description: "Timestamp when the usage resets", + example: 1759247877000, + }), + overage_allowed: z.boolean().nullish().meta({ + description: "Whether overage usage beyond the limit is allowed", + example: true, + }), + + breakdown: z + .array(ApiCusFeatureV3BreakdownSchema) + .nullish() + .meta({ + description: + "Detailed breakdown by interval for features with multiple intervals", + example: [ + { interval: "month", interval_count: 1, balance: 500, usage: 250 }, + ], + }), + credit_schema: z + .array( + z.object({ + feature_id: z.string().meta({ + description: "The ID of the feature that credits are applied to", + example: "feature_123", + }), + credit_amount: z.number().meta({ + description: "The amount of credits applied per usage", + example: 10, + }), + }), + ) + .nullish() + .meta({ + description: "Credit conversion schema for credit system features", + example: [{ feature_id: "feature_123", credit_amount: 10 }], + }), + + usage_limit: z.number().nullish().meta({ + description: "The maximum usage allowed", + example: 1000, + }), + rollovers: z + .array(ApiCusFeatureV3RolloverSchema) + .nullish() + .meta({ + description: "Array of rollover balances from previous periods", + example: [{ balance: 100, expires_at: 1759247877000 }], + }), +}); + +export const ApiCusFeatureV3Schema = z + .object({ + id: z.string().meta({ + description: "The unique identifier of the feature", + example: "feature_123", + }), + type: z.enum(ApiFeatureType).meta({ + description: "The type of the feature", + example: "single_use", + }), + name: z.string().nullish().meta({ + description: "The name of the feature", + example: "API Calls", + }), + }) + .extend(CoreCusFeatureSchema.shape); + +export type ApiCusFeatureV3 = z.infer; +export type ApiCusFeatureV3Rollover = z.infer< + typeof ApiCusFeatureV3RolloverSchema +>; +export type ApiCusFeatureV3Breakdown = z.infer< + typeof ApiCusFeatureV3BreakdownSchema +>; diff --git a/shared/api/customers/cusPlans/apiCusPlan.ts b/shared/api/customers/cusPlans/apiCusPlan.ts new file mode 100644 index 000000000..55b139888 --- /dev/null +++ b/shared/api/customers/cusPlans/apiCusPlan.ts @@ -0,0 +1,23 @@ +import { ApiPlanSchema } from "@api/products/apiPlan.js"; +import { z } from "zod/v4"; + +export const ApiCusPlanSchema = z.object({ + plan: ApiPlanSchema.optional(), + + plan_id: z.string(), + add_on: z.boolean(), + + // Flags / timestamps + status: z.enum(["active", "scheduled", "expired"]), + past_due: z.boolean(), + canceled_at: z.number().nullable(), + expires_at: z.number().nullable(), + trial_ends_at: z.number().nullable(), + + started_at: z.number(), + current_period_start: z.number().nullable(), + current_period_end: z.number().nullable(), + quantity: z.number(), +}); + +export type ApiCusPlan = z.infer; diff --git a/shared/api/customers/cusProducts/changes/V0_2_CusProductChange.ts b/shared/api/customers/cusPlans/changes/V0.2_CusProductChange.ts similarity index 95% rename from shared/api/customers/cusProducts/changes/V0_2_CusProductChange.ts rename to shared/api/customers/cusPlans/changes/V0.2_CusProductChange.ts index a5819589f..1133d0f2e 100644 --- a/shared/api/customers/cusProducts/changes/V0_2_CusProductChange.ts +++ b/shared/api/customers/cusPlans/changes/V0.2_CusProductChange.ts @@ -10,8 +10,11 @@ import { isPriceItem, } from "@utils/productV2Utils/productItemUtils/getItemType.js"; import { notNullish } from "@utils/utils.js"; -import { z } from "zod/v4"; -import type { CusProductLegacyData } from "../cusProductLegacyData.js"; +import type { z } from "zod/v4"; +import { + type CusProductLegacyData, + CusProductLegacyDataSchema, +} from "../cusProductLegacyData.js"; import { ApiCusProductV1Schema } from "../previousVersions/apiCusProductV1.js"; import { ApiCusProductV2Schema } from "../previousVersions/apiCusProductV2.js"; @@ -132,10 +135,6 @@ export function transformCusProductV2ToV1({ return v1CusProduct; } -const CusProductLegacyDataSchema = z.object({ - subscription_id: z.string().optional(), -}); - export const V0_2_CusProductChange = defineVersionChange({ newVersion: ApiVersion.V1_1, // Breaking change introduced in V1_1 oldVersion: ApiVersion.V0_2, // Applied when targetVersion <= V0_2 diff --git a/shared/api/customers/cusPlans/changes/V1.2_CusPlanChange.ts b/shared/api/customers/cusPlans/changes/V1.2_CusPlanChange.ts new file mode 100644 index 000000000..69a266190 --- /dev/null +++ b/shared/api/customers/cusPlans/changes/V1.2_CusPlanChange.ts @@ -0,0 +1,112 @@ +import type { ApiProductItem } from "@api/models.js"; +import { ApiVersion } from "@api/versionUtils/ApiVersion.js"; +import { + AffectedResource, + defineVersionChange, +} from "@api/versionUtils/versionChangeUtils/VersionChange.js"; +import { CusProductStatus } from "@models/cusProductModels/cusProductEnums.js"; +import { convertPlanToItems } from "@utils/planFeatureUtils/planToItems.js"; +import { getProductItemResponse } from "@utils/productV2Utils/productItemUtils/getProductItemRes.js"; +import type { z } from "zod/v4"; +import { type ApiCusPlan, ApiCusPlanSchema } from "../apiCusPlan.js"; +import { + type CusProductLegacyData, + CusProductLegacyDataSchema, +} from "../cusProductLegacyData.js"; +import { ApiCusProductV3Schema } from "../previousVersions/apiCusProductV3.js"; + +/** + * Transform plan from V2.0 format to V1.2 product format + * Exported so it can be reused in other transformations (e.g., V1_2_CustomerChange) + */ +export function transformCusPlanToProductV3({ + input, + legacyData, +}: { + input: z.infer; + legacyData?: CusProductLegacyData; +}): z.infer { + const cusPlanToCusProductV3Status = (plan: ApiCusPlan) => { + if (plan.status === CusProductStatus.Active) { + if (plan.past_due) { + return "past_due"; + } + // trialing + else if (plan.trial_ends_at && plan.trial_ends_at < Date.now()) { + return "trialing"; + } + return "active"; + } + return plan.status; + }; + + let items: ApiProductItem[] | null = null; + if (input.plan && legacyData) { + const productItems = convertPlanToItems({ plan: input.plan }); + + const itemResponses = productItems.map((item) => + getProductItemResponse({ + item, + features: legacyData.features, + options: legacyData.options, + }), + ); + + items = itemResponses; + } + + return { + id: input.plan_id, + + // Plan properties... + name: input.plan?.name ?? null, + group: input.plan?.group ?? null, + is_default: input.plan?.default ?? false, + is_add_on: input.add_on, + version: input.plan?.version ?? null, + items: items, // Map from plan to product v2 items... + + status: cusPlanToCusProductV3Status(input), + + canceled_at: input.canceled_at ?? null, + started_at: input.started_at, + current_period_start: input.current_period_start, + current_period_end: input.current_period_end, + + entity_id: null, + + quantity: input.quantity, + } satisfies z.infer; +} + +/** + * V1_2_CusPlanChange: Transforms customer plan response TO V1_2 format + * + * Applied when: targetVersion <= V1_2 + * + * Breaking changes introduced in V2.0: + * + * 1. Renamed "products" to "plans" in SDK + * 2. Simplified status enum to only "active" + * 3. Added trial_ends_at field + * 4. Optional product object for expanded plan data + * + * Input: ApiCusPlan (V2.0+ format with "product" field) + * Output: ApiCusProductV3 (V1.2 format with verbose fields) + */ +export const V1_2_CusPlanChange = defineVersionChange({ + newVersion: ApiVersion.V2, + oldVersion: ApiVersion.V1_2, + description: [ + "Renamed products to plans", + "Simplified status enum", + "Added trial_ends_at field", + ], + affectedResources: [AffectedResource.Customer], + newSchema: ApiCusPlanSchema, + oldSchema: ApiCusProductV3Schema, + + legacyDataSchema: CusProductLegacyDataSchema, + + transformResponse: transformCusPlanToProductV3, +}); diff --git a/shared/api/customers/cusPlans/cusProductLegacyData.ts b/shared/api/customers/cusPlans/cusProductLegacyData.ts new file mode 100644 index 000000000..53dd06939 --- /dev/null +++ b/shared/api/customers/cusPlans/cusProductLegacyData.ts @@ -0,0 +1,11 @@ +import { FeatureOptionsSchema } from "@models/cusProductModels/cusProductModels.js"; +import { FeatureSchema } from "@models/featureModels/featureModels.js"; +import { z } from "zod/v4"; + +export const CusProductLegacyDataSchema = z.object({ + subscription_id: z.string().optional(), + options: z.array(FeatureOptionsSchema), + features: z.array(FeatureSchema), +}); + +export type CusProductLegacyData = z.infer; diff --git a/shared/api/customers/cusProducts/previousVersions/apiCusProductV0.ts b/shared/api/customers/cusPlans/previousVersions/apiCusProductV0.ts similarity index 100% rename from shared/api/customers/cusProducts/previousVersions/apiCusProductV0.ts rename to shared/api/customers/cusPlans/previousVersions/apiCusProductV0.ts diff --git a/shared/api/customers/cusProducts/previousVersions/apiCusProductV1.ts b/shared/api/customers/cusPlans/previousVersions/apiCusProductV1.ts similarity index 100% rename from shared/api/customers/cusProducts/previousVersions/apiCusProductV1.ts rename to shared/api/customers/cusPlans/previousVersions/apiCusProductV1.ts diff --git a/shared/api/customers/cusProducts/previousVersions/apiCusProductV2.ts b/shared/api/customers/cusPlans/previousVersions/apiCusProductV2.ts similarity index 100% rename from shared/api/customers/cusProducts/previousVersions/apiCusProductV2.ts rename to shared/api/customers/cusPlans/previousVersions/apiCusProductV2.ts diff --git a/shared/api/customers/cusPlans/previousVersions/apiCusProductV3.ts b/shared/api/customers/cusPlans/previousVersions/apiCusProductV3.ts new file mode 100644 index 000000000..d07f0af94 --- /dev/null +++ b/shared/api/customers/cusPlans/previousVersions/apiCusProductV3.ts @@ -0,0 +1,96 @@ +import { ApiProductItemSchema } from "@api/products/planFeature/previousVersions/apiProductItem.js"; +import { z } from "zod/v4"; + +export const ApiCusProductV3Schema = z.object({ + id: z.string().meta({ + description: "The unique identifier for the product", + example: "pro_plan", + }), + name: z.string().nullable().meta({ + description: "The name of the product", + example: "Pro Plan", + }), + group: z.string().nullable().meta({ + description: "The group the product belongs to", + example: "product_set_1", + }), + status: z + .enum(["active", "expired", "scheduled", "trialing", "past_due"]) + .meta({ + description: "Current status of the product for this customer", + example: "active", + }), + canceled_at: z.number().nullish().meta({ + description: "Timestamp when the product was canceled for the customer", + example: 1717000000, + }), + started_at: z.number().meta({ + description: "Timestamp when the customer started this product", + example: 1700000000000, + }), + is_default: z.boolean().meta({ + description: "Whether this product is the default for the customer", + example: true, + }), + is_add_on: z.boolean().meta({ + description: "Whether the product is an add-on", + example: false, + }), + version: z.number().nullish().meta({ + description: "Version of the product", + example: 1, + }), + stripe_subscription_ids: z + .array(z.string()) + .nullish() + .meta({ + description: + "List of Stripe subscription IDs associated with this product, if any", + example: ["sub_1Nc0JzBAbcxyz", "sub_1Nc0xyBAnopq"], + }), + current_period_start: z.number().nullish().meta({ + description: "Start of the current billing period", + example: 1717000000, + }), + current_period_end: z.number().nullish().meta({ + description: "End of the current billing period", + example: 1719600000, + }), + entity_id: z.string().nullish().meta({ + description: + "ID of the entity this customer product is attached to, if applicable", + example: "entity_1234abcd", + }), + items: z + .array(ApiProductItemSchema) + .nullish() + .meta({ + description: "Array of product items defining the features and pricing", + example: [ + { + feature_id: "", + feature_type: "single_use", + included_usage: 123, + interval: "month", + usage_model: "prepaid", + price: 123, + billing_units: 1000, + entity_feature_id: "", + reset_usage_when_enabled: true, + tiers: [ + { + to: 100, + amount: 10, + }, + ], + }, + ], + }), + quantity: z.number().optional().meta({ + description: + "The number of units of this product held by the customer, if applicable", + example: 1, + }), +}); + +export type ApiCusProductV3 = z.infer; diff --git a/shared/api/customers/cusProducts/apiCusProduct.ts b/shared/api/customers/cusProducts/apiCusProduct.ts deleted file mode 100644 index 48d419ae6..000000000 --- a/shared/api/customers/cusProducts/apiCusProduct.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { ApiProductItemSchema } from "@api/products/planFeature/previousVersions/apiProductItem.js"; -import { z } from "zod/v4"; - -export const ApiCusProductSchema = z - .object({ - id: z.string().meta({ - description: "The unique identifier for the product", - example: "pro_plan", - }), - name: z.string().nullable().meta({ - description: "The name of the product", - example: "Pro Plan", - }), - group: z.string().nullable().meta({ - description: "The group the product belongs to", - example: "product_set_1", - }), - status: z - .enum(["active", "expired", "scheduled", "trialing", "past_due"]) - .meta({ - description: "Current status of the product for this customer", - example: "active", - }), - canceled_at: z.number().nullish().meta({ - description: "Timestamp when the product was canceled for the customer", - example: 1717000000, - }), - started_at: z.number().meta({ - description: "Timestamp when the customer started this product", - example: 1700000000000, - }), - is_default: z.boolean().meta({ - description: "Whether this product is the default for the customer", - example: true, - }), - is_add_on: z.boolean().meta({ - description: "Whether the product is an add-on", - example: false, - }), - version: z.number().nullish().meta({ - description: "Version of the product", - example: 1, - }), - stripe_subscription_ids: z - .array(z.string()) - .nullish() - .meta({ - description: - "List of Stripe subscription IDs associated with this product, if any", - example: ["sub_1Nc0JzBAbcxyz", "sub_1Nc0xyBAnopq"], - }), - current_period_start: z.number().nullish().meta({ - description: "Start of the current billing period", - example: 1717000000, - }), - current_period_end: z.number().nullish().meta({ - description: "End of the current billing period", - example: 1719600000, - }), - entity_id: z.string().nullish().meta({ - description: - "ID of the entity this customer product is attached to, if applicable", - example: "entity_1234abcd", - }), - items: z - .array(ApiProductItemSchema) - .nullish() - .meta({ - description: "Array of product items defining the features and pricing", - example: [ - { - feature_id: "", - feature_type: "single_use", - included_usage: 123, - interval: "month", - usage_model: "prepaid", - price: 123, - billing_units: 1000, - entity_feature_id: "", - reset_usage_when_enabled: true, - tiers: [ - { - to: 100, - amount: 10, - }, - ], - }, - ], - }), - quantity: z.number().optional().meta({ - description: - "The number of units of this product held by the customer, if applicable", - example: 1, - }), - }); - -export type ApiCusProduct = z.infer; diff --git a/shared/api/customers/cusProducts/cusProductLegacyData.ts b/shared/api/customers/cusProducts/cusProductLegacyData.ts deleted file mode 100644 index 9365a877f..000000000 --- a/shared/api/customers/cusProducts/cusProductLegacyData.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { z } from "zod/v4"; - -export const CusProductLegacyDataSchema = z.object({ - subscription_id: z.string().optional(), -}); - -export type CusProductLegacyData = z.infer; diff --git a/shared/api/customers/customerLegacyData.ts b/shared/api/customers/customerLegacyData.ts index 86fd61b05..89ef0067d 100644 --- a/shared/api/customers/customerLegacyData.ts +++ b/shared/api/customers/customerLegacyData.ts @@ -1,5 +1,5 @@ import { z } from "zod/v4"; -import { CusProductLegacyDataSchema } from "./cusProducts/cusProductLegacyData.js"; +import { CusProductLegacyDataSchema } from "./cusPlans/cusProductLegacyData.js"; export const CustomerLegacyDataSchema = z.object({ cusProductLegacyData: z.record(z.string(), CusProductLegacyDataSchema), diff --git a/shared/api/customers/previousVersions/apiCustomerV0.ts b/shared/api/customers/previousVersions/apiCustomerV0.ts index e526e7296..46feb6433 100644 --- a/shared/api/customers/previousVersions/apiCustomerV0.ts +++ b/shared/api/customers/previousVersions/apiCustomerV0.ts @@ -1,9 +1,9 @@ import { ApiCusFeatureV0Schema } from "@api/customers/cusFeatures/previousVersions/apiCusFeatureV0.js"; -import { ApiCusProductV0Schema } from "@api/customers/cusProducts/previousVersions/apiCusProductV0.js"; +import { ApiCusProductV0Schema } from "@api/customers/cusPlans/previousVersions/apiCusProductV0.js"; import { ApiInvoiceSchema } from "@api/others/apiInvoice.js"; import { AppEnv } from "@models/genModels/genEnums.js"; import { z } from "zod/v4"; -import { ApiTrialsUsedSchema } from "../apiCustomer.js"; +import { ApiTrialsUsedSchema } from "./apiCustomerV3.js"; /** * ApiCustomerV0Schema - Customer response format for API V0_1 diff --git a/shared/api/customers/previousVersions/apiCustomerV1.ts b/shared/api/customers/previousVersions/apiCustomerV1.ts index 6e40e3f9a..bfb9ca820 100644 --- a/shared/api/customers/previousVersions/apiCustomerV1.ts +++ b/shared/api/customers/previousVersions/apiCustomerV1.ts @@ -2,8 +2,8 @@ import { ApiCusFeatureV1Schema } from "@api/customers/cusFeatures/previousVersio import { ApiInvoiceSchema } from "@api/others/apiInvoice.js"; import { AppEnv } from "@models/genModels/genEnums.js"; import { z } from "zod/v4"; -import { ApiTrialsUsedSchema } from "../apiCustomer.js"; -import { ApiCusProductV1Schema } from "../cusProducts/previousVersions/apiCusProductV1.js"; +import { ApiCusProductV1Schema } from "../cusPlans/previousVersions/apiCusProductV1.js"; +import { ApiTrialsUsedSchema } from "./apiCustomerV3.js"; /** * ApiCustomerV1Schema - Customer response format for API V0_2/V1_0/V1_1 (pre V1_2) diff --git a/shared/api/customers/previousVersions/apiCustomerV2.ts b/shared/api/customers/previousVersions/apiCustomerV2.ts index 65f5bb9da..204a4fadd 100644 --- a/shared/api/customers/previousVersions/apiCustomerV2.ts +++ b/shared/api/customers/previousVersions/apiCustomerV2.ts @@ -1,7 +1,7 @@ import { ApiCusReferralSchema } from "@api/customers/components/apiCusReferral.js"; import { ApiCusUpcomingInvoiceSchema } from "@api/customers/components/apiCusUpcomingInvoice.js"; import { ApiCusFeatureV2Schema } from "@api/customers/cusFeatures/previousVersions/apiCusFeatureV2.js"; -import { ApiCusProductV2Schema } from "@api/customers/cusProducts/previousVersions/apiCusProductV2.js"; +import { ApiCusProductV2Schema } from "@api/customers/cusPlans/previousVersions/apiCusProductV2.js"; import { ApiEntitySchema } from "@api/entities/apiEntity.js"; import { ApiCusRewardsSchema } from "@api/models.js"; import { ApiInvoiceSchema } from "@api/others/apiInvoice.js"; diff --git a/shared/api/customers/previousVersions/apiCustomerV3.ts b/shared/api/customers/previousVersions/apiCustomerV3.ts new file mode 100644 index 000000000..d0f2e07f4 --- /dev/null +++ b/shared/api/customers/previousVersions/apiCustomerV3.ts @@ -0,0 +1,71 @@ +import { ApiCusReferralSchema } from "@api/customers/components/apiCusReferral.js"; +import { ApiBaseEntitySchema } from "@api/entities/apiEntity.js"; +import { ApiCusRewardsSchema } from "@api/others/apiDiscount.js"; +import { ApiInvoiceSchema } from "@api/others/apiInvoice.js"; +import { AppEnv } from "@models/genModels/genEnums.js"; +import { z } from "zod/v4"; +import { ApiCusUpcomingInvoiceSchema } from "../components/apiCusUpcomingInvoice.js"; +import { ApiCusFeatureV3Schema } from "../cusFeatures/previousVersions/apiCusFeatureV3.js"; +import { ApiCusProductV3Schema } from "../cusPlans/previousVersions/apiCusProductV3.js"; + +export const ApiTrialsUsedSchema = z.object({ + product_id: z.string(), + customer_id: z.string(), + fingerprint: z.string().nullish(), +}); + +export const ApiCusExpandV3Schema = z.object({ + invoices: z.array(ApiInvoiceSchema).optional(), + entities: z.array(ApiBaseEntitySchema).optional(), + trials_used: z.array(ApiTrialsUsedSchema).optional(), + rewards: ApiCusRewardsSchema.nullish(), + referrals: z.array(ApiCusReferralSchema).optional(), + upcoming_invoice: ApiCusUpcomingInvoiceSchema.nullish(), + payment_method: z.any().nullish(), +}); + +export const ApiCustomerV3Schema = z.object({ + // Internal fields + id: z.string().nullable().meta({ + description: "Your internal ID for the customer", + example: "cus_123", + }), + created_at: z.number().meta({ + description: "Timestamp of customer creation in milliseconds since epoch", + example: 1717000000, + }), + name: z.string().nullable().meta({ + description: "Customer's name", + example: "John Doe", + }), + email: z.string().nullable().meta({ + description: "Customer's email address", + example: "john@doe.com", + }), + fingerprint: z.string().nullable().meta({ + description: + "Unique identifier (eg. serial number) to detect duplicate customers and prevent key leaks", + example: "fp_9184And92839123hda", + }), + stripe_id: z.string().nullable().default(null).meta({ + description: "Stripe customer ID", + example: "cus_J8A5c31A8tlpwN", + }), + env: z.enum(AppEnv).meta({ + description: "Environment the customer is in", + example: "production", + }), + metadata: z.record(z.any(), z.any()).default({}), + products: z.array(ApiCusProductV3Schema).meta({ + description: "List of products the customer has access to", + example: [], + }), + features: z.record(z.string(), ApiCusFeatureV3Schema).meta({ + description: "List of features the customer has access to", + example: {}, + }), + ...ApiCusExpandV3Schema.shape, +}); + +export type ApiCustomerV3 = z.infer; +export type ApiCustomerV3Expand = z.infer; diff --git a/shared/api/customers/requestChanges/V1.2_CustomerQueryChange.ts b/shared/api/customers/requestChanges/V1.2_CustomerQueryChange.ts new file mode 100644 index 000000000..33e997a09 --- /dev/null +++ b/shared/api/customers/requestChanges/V1.2_CustomerQueryChange.ts @@ -0,0 +1,60 @@ +import { ApiVersion } from "@api/versionUtils/ApiVersion.js"; +import { + AffectedResource, + defineVersionChange, +} from "@api/versionUtils/versionChangeUtils/VersionChange.js"; +import { CusExpand } from "@models/cusModels/cusExpand.js"; +import type { z } from "zod/v4"; +import { GetCustomerQuerySchema } from "../customerOpModels.js"; + +/** + * V1_2_CustomerQueryChange: Transforms customer query TO latest format + * + * Applied when: sourceVersion <= V1_2 + * + * Changes introduced in V2.0: + * + * 1. Added new expand options: + * - "plans.plan" - expands the full plan object in each customer plan + * - "features.feature" - expands the full feature object in each customer feature + * + * This transformation automatically adds these expand options when transforming + * from V1.2 to V2.0, so that V1.2 clients get the full plan/feature objects + * (matching their expected verbose format). + * + * Input: Query without plans.plan/features.feature expand + * Output: Query with plans.plan and features.feature automatically expanded + */ + +export const V1_2_CustomerQueryChange = defineVersionChange({ + newVersion: ApiVersion.V2, + oldVersion: ApiVersion.V1_Beta, + description: [ + "Automatically expands plans.plan and features.feature for V1.2 clients", + "Ensures V1.2 clients get full plan/feature objects", + ], + affectedResources: [AffectedResource.Customer], + newSchema: GetCustomerQuerySchema, + oldSchema: GetCustomerQuerySchema, + + // Only transform requests (incoming queries) + affectsRequest: true, + affectsResponse: false, + + // Request: V1.2 → V2.0 (add expand options) + transformRequest: ({ + input, + }: { + input: z.infer; + }) => { + const existingExpand = input.expand || []; + + // Add `plan` and `feature` to expand array + const newExpand = [...existingExpand, CusExpand.Plan, CusExpand.Feature]; + + return { + ...input, + expand: newExpand, + } satisfies z.infer; + }, +}); diff --git a/shared/api/entities/apiEntity.ts b/shared/api/entities/apiEntity.ts index c416c9309..e58075ca1 100644 --- a/shared/api/entities/apiEntity.ts +++ b/shared/api/entities/apiEntity.ts @@ -1,5 +1,5 @@ -import { ApiCusFeatureSchema } from "@api/customers/cusFeatures/apiCusFeature.js"; -import { ApiCusProductSchema } from "@api/customers/cusProducts/apiCusProduct.js"; +import { ApiCusFeatureV3Schema } from "@api/customers/cusFeatures/previousVersions/apiCusFeatureV3.js"; +import { ApiCusPlanSchema } from "@api/customers/cusPlans/apiCusPlan.js"; import { ApiInvoiceSchema } from "@api/others/apiInvoice.js"; import { AppEnv } from "@models/genModels/genEnums.js"; import { z } from "zod/v4"; @@ -29,11 +29,11 @@ export const ApiBaseEntitySchema = z.object({ }); export const ApiEntitySchema = ApiBaseEntitySchema.extend({ - products: z.array(ApiCusProductSchema).optional().meta({ - description: "Products associated with this entity", + plans: z.array(ApiCusPlanSchema).optional().meta({ + description: "Plans associated with this entity", example: [], }), - features: z.record(z.string(), ApiCusFeatureSchema).optional().meta({ + features: z.record(z.string(), ApiCusFeatureV3Schema).optional().meta({ description: "Features associated with this entity", example: {}, }), @@ -44,4 +44,4 @@ export const ApiEntitySchema = ApiBaseEntitySchema.extend({ }), }); -export type EntityResponse = z.infer; +export type ApiEntity = z.infer; diff --git a/shared/api/models.ts b/shared/api/models.ts index 0f1124626..662061d66 100644 --- a/shared/api/models.ts +++ b/shared/api/models.ts @@ -18,8 +18,8 @@ export * from "./customers/cusFeatures/apiCusFeature.js"; export * from "./customers/cusFeatures/previousVersions/apiCusFeatureV0.js"; export * from "./customers/cusFeatures/previousVersions/apiCusFeatureV1.js"; export * from "./customers/cusFeatures/previousVersions/apiCusFeatureV2.js"; -export * from "./customers/cusProducts/apiCusProduct.js"; -export * from "./customers/cusProducts/cusProductLegacyData.js"; +export * from "./customers/cusPlans/apiCusPlan.js"; +export * from "./customers/cusPlans/cusProductLegacyData.js"; export * from "./customers/customerLegacyData.js"; export * from "./customers/customerOpModels.js"; export * from "./customers/previousVersions/apiCustomerV2.js"; diff --git a/shared/api/openapi.ts b/shared/api/openapi.ts index 25b052b9d..55d2f8b73 100644 --- a/shared/api/openapi.ts +++ b/shared/api/openapi.ts @@ -9,7 +9,7 @@ import { CustomerDataSchema } from "./common/customerData.js"; import { EntityDataSchema } from "./common/entityData.js"; import { coreOps } from "./core/coreOpenApi.js"; import { ApiCusFeatureSchema } from "./customers/cusFeatures/apiCusFeature.js"; -import { ApiCusProductSchema } from "./customers/cusProducts/apiCusProduct.js"; +import { ApiCusPlanSchema } from "./customers/cusPlans/apiCusPlan.js"; import { ApiCustomerWithMeta, customerOps, @@ -65,9 +65,9 @@ const document = createDocument({ id: "Customer", description: "Customer object returned by the API", }), - CustomerProduct: ApiCusProductSchema.meta({ - id: "CustomerProduct", - description: "Customer product object returned by the API", + CustomerPlan: ApiCusPlanSchema.meta({ + id: "CustomerPlan", + description: "Customer plan object returned by the API", }), CustomerFeature: ApiCusFeatureSchema.meta({ id: "CustomerFeature", @@ -79,7 +79,8 @@ const document = createDocument({ }), ProductItem: ApiProductItemWithMeta.meta({ id: "ProductItem", - description: "Product item defining features and pricing within a product", + description: + "Product item defining features and pricing within a product", }), Feature: ApiFeatureWithMeta.meta({ id: "Feature", diff --git a/shared/api/products/apiPlan.ts b/shared/api/products/apiPlan.ts index e3e4b6d7a..510713cb8 100644 --- a/shared/api/products/apiPlan.ts +++ b/shared/api/products/apiPlan.ts @@ -27,10 +27,15 @@ export const ApiPlanSchema = z.object({ add_on: z.boolean(), default: z.boolean(), - price: z.object({ - amount: z.number(), - interval: z.enum(BillingInterval), - }), + // Change + price: z + .object({ + amount: z.number(), + interval: z.enum(BillingInterval), + interval_count: z.number().optional(), + // tiers: z.array(UsageTierSchema).optional(), + }) + .nullable(), features: z.array(ApiPlanFeatureSchema), free_trial: ApiFreeTrialV2Schema.nullable().optional(), diff --git a/shared/api/products/changes/V1.2_ProductChanges.ts b/shared/api/products/changes/V1.2_ProductChanges.ts index 0918f8510..14b83c056 100644 --- a/shared/api/products/changes/V1.2_ProductChanges.ts +++ b/shared/api/products/changes/V1.2_ProductChanges.ts @@ -40,7 +40,7 @@ import { export const V1_2_ProductChanges = defineVersionChange({ newVersion: ApiVersion.V2, // Breaking change introduced in V2 - oldVersion: ApiVersion.V1_2, // Applied when targetVersion <= V1.2 + oldVersion: ApiVersion.V1_Beta, // Applied when targetVersion <= V1.2 description: [ "Product format changed from Plan to ProductV2 schema", "Features array renamed to items with different structure", diff --git a/shared/api/products/planFeature/apiPlanFeature.ts b/shared/api/products/planFeature/apiPlanFeature.ts index a8bcb1da5..f1a5cdd35 100644 --- a/shared/api/products/planFeature/apiPlanFeature.ts +++ b/shared/api/products/planFeature/apiPlanFeature.ts @@ -11,12 +11,11 @@ import { ResetInterval } from "../planEnums.js"; export const ApiPlanFeatureSchema = z .object({ feature_id: z.string(), - granted: z.number(), + granted_balance: z.number(), unlimited: z.boolean(), - reset_interval: z.enum(ResetInterval).optional(), reset_interval_count: z.number().optional(), - reset_usage_on_enabled: z.boolean().optional(), + reset_usage_when_enabled: z.boolean().optional(), price: z .object({ @@ -48,6 +47,13 @@ export const ApiPlanFeatureSchema = z expiry_duration_length: z.number().optional(), }) .optional(), + + display: z + .object({ + primary_text: z.string(), + secondary_text: z.string().optional(), + }) + .optional(), }) .check((ctx) => { const resetGroup = diff --git a/shared/api/versionUtils/ApiVersion.ts b/shared/api/versionUtils/ApiVersion.ts index 538c32017..702f5837b 100644 --- a/shared/api/versionUtils/ApiVersion.ts +++ b/shared/api/versionUtils/ApiVersion.ts @@ -5,7 +5,7 @@ */ export enum ApiVersion { V2 = "2.0.0", - Beta = "beta", + V1_Beta = "beta", V1_2 = "1.2.0", V1_1 = "1.1.0", V0_2 = "0.2.0", diff --git a/shared/api/versionUtils/convertVersionUtils.ts b/shared/api/versionUtils/convertVersionUtils.ts index a4fe84b9e..dc3568dfe 100644 --- a/shared/api/versionUtils/convertVersionUtils.ts +++ b/shared/api/versionUtils/convertVersionUtils.ts @@ -52,7 +52,7 @@ export function legacyToSemVer({ return ApiVersion.V1_2; case LegacyVersion.v1_4: case 1.4: - return ApiVersion.Beta; + return ApiVersion.V1_Beta; default: return null; } @@ -76,7 +76,7 @@ export function semVerToLegacy({ return LegacyVersion.v1_1; case ApiVersion.V1_2: return LegacyVersion.v1_2; - case ApiVersion.Beta: + case ApiVersion.V1_Beta: return LegacyVersion.v1_4; default: return null; @@ -86,7 +86,10 @@ export function semVerToLegacy({ /** * Parse version string (CalVer, SemVer, or legacy) * Supports .clover suffix: "2025-04-17.clover" + * Supports partial SemVer: "2" → "2.0.0", "2.0" → "2.0.0" * @example parseVersion({ versionStr: "2025-04-17" }) // ApiVersion.V1_1 + * @example parseVersion({ versionStr: "2" }) // ApiVersion.V2 + * @example parseVersion({ versionStr: "2.0" }) // ApiVersion.V2 */ export function parseVersion({ versionStr, @@ -98,7 +101,18 @@ export function parseVersion({ return calVerToSemVer({ calver: versionStr }); } - // SemVer (X.Y.Z) + // SemVer (X.Y.Z) - normalize partial versions + // Supports "2" → "2.0.0", "2.0" → "2.0.0", "2.0.0" → "2.0.0" + if (/^\d+(\.\d+)*$/.test(versionStr)) { + const parts = versionStr.split("."); + const normalizedVersion = `${parts[0] || "0"}.${parts[1] || "0"}.${parts[2] || "0"}`; + + if (Object.values(ApiVersion).includes(normalizedVersion as ApiVersion)) { + return normalizedVersion as ApiVersion; + } + } + + // Check for exact match (for special versions like "beta") if (Object.values(ApiVersion).includes(versionStr as ApiVersion)) { return versionStr as ApiVersion; } @@ -143,8 +157,8 @@ export function createdAtToVersion({ version = ApiVersion.V0_1; } - // return new ApiVersionClass(version); - return new ApiVersionClass(ApiVersion.V2); + return new ApiVersionClass(version); + // return new ApiVersionClass(ApiVersion.V2); } // Convert org creation date diff --git a/shared/api/versionUtils/versionChangeUtils/VersionChangeRegistryClass.ts b/shared/api/versionUtils/versionChangeUtils/VersionChangeRegistryClass.ts index 112922a28..e973142e1 100644 --- a/shared/api/versionUtils/versionChangeUtils/VersionChangeRegistryClass.ts +++ b/shared/api/versionUtils/versionChangeUtils/VersionChangeRegistryClass.ts @@ -9,7 +9,6 @@ import type { * Maps versions → change classes */ const changes = new Map(); -const instances = new Map(); export const VersionChangeRegistryClass = { register({ @@ -24,17 +23,8 @@ export const VersionChangeRegistryClass = { getChangesForVersion({ version }: { version: ApiVersion }): VersionChange[] { const changeClasses = changes.get(version) || []; - return changeClasses.map((ChangeClass) => { - const key = `${version}-${ChangeClass.name}`; - if (!instances.has(key)) { - instances.set(key, new ChangeClass()); - } - const instance = instances.get(key); - if (!instance) { - throw new Error(`Failed to create instance for ${ChangeClass.name}`); - } - return instance; - }); + // Simply instantiate each change class - no caching needed since they're stateless + return changeClasses.map((ChangeClass) => new ChangeClass()); }, getRegisteredVersions(): ApiVersion[] { @@ -54,7 +44,6 @@ export const VersionChangeRegistryClass = { clear() { changes.clear(); - instances.clear(); }, }; diff --git a/shared/api/versionUtils/versionChangeUtils/applyVersionChanges.ts b/shared/api/versionUtils/versionChangeUtils/applyVersionChanges.ts index 80fe04295..1489fcaf8 100644 --- a/shared/api/versionUtils/versionChangeUtils/applyVersionChanges.ts +++ b/shared/api/versionUtils/versionChangeUtils/applyVersionChanges.ts @@ -1,7 +1,13 @@ import { type ApiVersion, LATEST_VERSION } from "../ApiVersion.js"; import { ApiVersionClass } from "../ApiVersionClass.js"; -import { getVersionsBetween } from "../versionRegistryUtils.js"; -import type { AffectedResource } from "./VersionChange.js"; +import { + getVersionsBetween, + getVersionsForRequestTransform, +} from "../versionRegistryUtils.js"; +import type { + AffectedResource, + VersionChangeConstructor, +} from "./VersionChange.js"; import { VersionChangeRegistryClass } from "./VersionChangeRegistryClass.js"; /** @@ -33,6 +39,7 @@ export function applyResponseVersionChanges({ }): T { // Default currentVersion to latest if not provided const _currentVersion = currentVersion || new ApiVersionClass(LATEST_VERSION); + // If versions are equal, no transformation needed if (_currentVersion.eq(targetVersion)) { return input; @@ -58,6 +65,7 @@ export function applyResponseVersionChanges({ // Apply each version's changes let transformedData = input; + for (const version of versionsToApply) { const changes = VersionChangeRegistryClass.getChangesForVersion({ version, @@ -88,7 +96,7 @@ export function applyResponseVersionChanges({ ? change.description.join("; ") : change.description; - // console.log(`Applying change [${change.oldVersion}]: ${description}`); + console.log(`Applying change [${change.oldVersion}]: ${description}`); // Apply the response transformation (backward) transformedData = change.transformResponse({ input: transformedData, @@ -127,34 +135,34 @@ export function applyResponseVersionChanges({ export function applyRequestVersionChanges({ input, legacyData, - targetVersion, - currentVersion, + fromVersion, + toVersion, resource, }: { input: T; legacyData?: TLegacyData; - targetVersion: ApiVersionClass; // User's version (old) - currentVersion: ApiVersionClass; // Latest version (new) + fromVersion: ApiVersionClass; // User's version (old) + toVersion: ApiVersionClass; // Latest version (new) resource: AffectedResource; }): T { // If versions are equal, no transformation needed - if (targetVersion.eq(currentVersion)) { + if (fromVersion.eq(toVersion)) { return input; } // If target is newer than current, throw error - if (targetVersion.gt(currentVersion)) { + if (fromVersion.gt(toVersion)) { throw new Error( - `Cannot transform forward from ${currentVersion} to ${targetVersion}. ` + + `Cannot transform forward from ${fromVersion} to ${toVersion}. ` + "Current version should be >= target version.", ); } // Get all versions between target and current (exclusive of current, inclusive of target) - const versionsToApply = getVersionsBetween({ - from: targetVersion.value, - to: currentVersion.value, - }).filter((v) => v !== currentVersion.value); // Exclude current itself + const versionsToApply = getVersionsForRequestTransform({ + from: fromVersion.value, + to: toVersion.value, + }); // Don't reverse - we want to go forward (old → new) // versionsToApply is already in ascending order from getVersionsBetween @@ -213,7 +221,7 @@ export function backwardsChangeActive({ versionChange, }: { apiVersion: ApiVersionClass; - versionChange: new () => any; + versionChange: VersionChangeConstructor; }): boolean { const change = new versionChange(); return apiVersion.lte(change.oldVersion); @@ -254,22 +262,22 @@ export function applyResponseVersionChangesToArray({ export function applyRequestVersionChangesToArray({ inputArray, legacyData, - targetVersion, - currentVersion, + fromVersion, + toVersion, resource, }: { inputArray: T[]; legacyData?: TLegacyData; - targetVersion: ApiVersionClass; - currentVersion: ApiVersionClass; + fromVersion: ApiVersionClass; + toVersion: ApiVersionClass; resource: AffectedResource; }): T[] { return inputArray.map((item) => applyRequestVersionChanges({ input: item, legacyData, - targetVersion, - currentVersion, + fromVersion, + toVersion, resource, }), ); diff --git a/shared/api/versionUtils/versionChangeUtils/versionChangeRegistry.ts b/shared/api/versionUtils/versionChangeUtils/versionChangeRegistry.ts index 3a0f391ba..b7cc53274 100644 --- a/shared/api/versionUtils/versionChangeUtils/versionChangeRegistry.ts +++ b/shared/api/versionUtils/versionChangeUtils/versionChangeRegistry.ts @@ -4,16 +4,20 @@ import { V0_1_CustomerChange } from "@api/customers/changes/V0.1_CustomerChange. import { V0_2_CustomerChange } from "@api/customers/changes/V0.2_CustomerChange.js"; import { V0_2_InvoicesAlwaysExpanded } from "@api/customers/changes/V0.2_InvoicesAlwaysExpanded.js"; import { V1_1_FeaturesArrayToObject } from "@api/customers/changes/V1.1_FeaturesArrayToObject.js"; + // Import customer product changes +import { V1_2_CustomerChange } from "@api/customers/changes/V1.2_CustomerChange.js"; +import { V1_2_CustomerQueryChange } from "@api/customers/requestChanges/V1.2_CustomerQueryChange.js"; // Import product changes import { V1_2_ProductChanges } from "@api/products/changes/V1.2_ProductChanges.js"; - import { ApiVersion } from "../ApiVersion.js"; import type { VersionChangeConstructor } from "./VersionChange.js"; import { VersionChangeRegistryClass } from "./VersionChangeRegistryClass.js"; export const V2_CHANGES: VersionChangeConstructor[] = [ + V1_2_CustomerChange, // Transforms Customer TO V1.2 format from V2 format + V1_2_CustomerQueryChange, // Transforms Customer Query TO V2.0 format (adds expand options) V1_2_ProductChanges, // Transforms Product TO V1.2 format from V2 Plan format ]; @@ -42,7 +46,7 @@ export function registerAllVersionChanges() { changes: V2_CHANGES, }); VersionChangeRegistryClass.register({ - version: ApiVersion.Beta, + version: ApiVersion.V1_Beta, changes: V1_4_CHANGES, }); VersionChangeRegistryClass.register({ diff --git a/shared/api/versionUtils/versionRegistry.ts b/shared/api/versionUtils/versionRegistry.ts index 73927245c..5d6369a45 100644 --- a/shared/api/versionUtils/versionRegistry.ts +++ b/shared/api/versionUtils/versionRegistry.ts @@ -20,8 +20,9 @@ export const VERSION_REGISTRY: Record = { releasedAt: new Date("2025-10-16").getTime(), description: "Plan features with max_purchase", }, - [ApiVersion.Beta]: { - semver: ApiVersion.Beta, + + [ApiVersion.V1_Beta]: { + semver: ApiVersion.V1_Beta, calver: "beta", releasedAt: new Date("2025-06-01").getTime(), description: "Beta version with experimental features", diff --git a/shared/api/versionUtils/versionRegistryUtils.ts b/shared/api/versionUtils/versionRegistryUtils.ts index 42f63613a..fb74d61cf 100644 --- a/shared/api/versionUtils/versionRegistryUtils.ts +++ b/shared/api/versionUtils/versionRegistryUtils.ts @@ -53,3 +53,21 @@ export function getVersionsBetween({ return sorted.slice(fromIndex, toIndex + 1); } + +export function getVersionsForRequestTransform({ + from, + to, +}: { + from: ApiVersion; + to: ApiVersion; +}): ApiVersion[] { + const sorted = getVersionsSorted(); + const fromIndex = sorted.indexOf(from); + const toIndex = sorted.indexOf(to); + + if (fromIndex === -1 || toIndex === -1) { + throw new Error(`Invalid version range: ${from} to ${to}`); + } + + return sorted.slice(fromIndex + 1, toIndex + 2); +} diff --git a/shared/models/cusModels/cusExpand.ts b/shared/models/cusModels/cusExpand.ts index 3495026e4..5a9943a68 100644 --- a/shared/models/cusModels/cusExpand.ts +++ b/shared/models/cusModels/cusExpand.ts @@ -6,4 +6,8 @@ export enum CusExpand { Referrals = "referrals", PaymentMethod = "payment_method", UpcomingInvoice = "upcoming_invoice", + // PlansPlan = "plans.plan", + // FeaturesFeature = "features.feature", + Plan = "plan", + Feature = "feature", } diff --git a/shared/utils/cusEntUtils/convertCusEntUtils.ts b/shared/utils/cusEntUtils/convertCusEntUtils.ts index 97f2d20b5..6cc2bf3d6 100644 --- a/shared/utils/cusEntUtils/convertCusEntUtils.ts +++ b/shared/utils/cusEntUtils/convertCusEntUtils.ts @@ -5,6 +5,7 @@ import { type FullCustomerEntitlement, getCusEntBalance, getStartingBalance, + notNullish, } from "../../index.js"; import { cusEntToCusPrice } from "../productUtils/convertUtils.js"; import { getRolloverFields } from "./getRolloverFields.js"; @@ -93,7 +94,7 @@ export const cusEntToIncludedUsage = ({ // } }; -export const cusEntToUsageLimit = ({ +export const cusEntToMaxPurchase = ({ cusEnt, }: { cusEnt: FullCusEntWithFullCusProduct; @@ -102,6 +103,13 @@ export const cusEntToUsageLimit = ({ cusEnt, }); - if (cusEnt.entitlement.usage_limit) return cusEnt.entitlement.usage_limit; - return startingBalance; + const usageLimit = cusEnt.entitlement.usage_limit; + // if (cusEnt.entitlement.usage_limit) return cusEnt.entitlement.usage_limit; + // return startingBalance; + + if (notNullish(usageLimit) && notNullish(startingBalance)) { + return new Decimal(usageLimit).sub(startingBalance).toNumber(); + } + + return 0; }; diff --git a/shared/utils/cusUtils/cusPlanUtils/cusPlanUtils.ts b/shared/utils/cusUtils/cusPlanUtils/cusPlanUtils.ts new file mode 100644 index 000000000..543d02fe6 --- /dev/null +++ b/shared/utils/cusUtils/cusPlanUtils/cusPlanUtils.ts @@ -0,0 +1,20 @@ +import { CusProductStatus } from "@models/cusProductModels/cusProductEnums.js"; + +export const cusProductToPlanStatus = ({ + status, +}: { + status: CusProductStatus; +}) => { + switch (status) { + case CusProductStatus.Active: + return "active"; + case CusProductStatus.PastDue: + return "active"; + case CusProductStatus.Expired: + return "expired"; + case CusProductStatus.Scheduled: + return "scheduled"; + default: + return "active"; + } +}; diff --git a/shared/utils/index.ts b/shared/utils/index.ts index 331446e96..f0a0a3171 100644 --- a/shared/utils/index.ts +++ b/shared/utils/index.ts @@ -14,6 +14,8 @@ export * from "./cusProductUtils/cusProductConstants.js"; export * from "./cusProductUtils/cusProductUtils.js"; export * from "./cusProductUtils/formatCusProductUtils.js"; export * from "./cusProductUtils/productIdToCusProduct.js"; +// Cus utils +export * from "./cusUtils/cusPlanUtils/cusPlanUtils.js"; export * from "./featureUtils/apiFeatureToDbFeature.js"; export * from "./featureUtils/convertFeatureUtils.js"; // Feature utils diff --git a/shared/utils/planFeatureUtils/planFeatureIntervals.ts b/shared/utils/planFeatureUtils/planFeatureIntervals.ts index 745d913a2..971fe4e86 100644 --- a/shared/utils/planFeatureUtils/planFeatureIntervals.ts +++ b/shared/utils/planFeatureUtils/planFeatureIntervals.ts @@ -1,4 +1,5 @@ import { ResetInterval } from "@api/models.js"; +import { EntInterval } from "@models/productModels/entModels/entEnums.js"; import { ProductItemInterval, RolloverDuration, @@ -85,3 +86,38 @@ export const rolloverToResetIntv = ( return ResetInterval.Month; } }; + +export const entIntvToResetIntv = ({ + entInterval, +}: { + entInterval: EntInterval | null | undefined; +}) => { + if (entInterval === EntInterval.Lifetime || !entInterval) { + return ResetInterval.OneOff; + } + + return entInterval as unknown as ResetInterval; +}; + +export const resetIntvToEntIntv = ({ + resetIntv, +}: { + resetIntv: ResetInterval; +}) => { + if (resetIntv === ResetInterval.OneOff) { + return EntInterval.Lifetime; + } + + return resetIntv as unknown as EntInterval; +}; + +export const toIntervalCountResponse = ({ + intervalCount, +}: { + intervalCount: number | undefined; +}) => { + if (intervalCount === 1) { + return undefined; + } + return intervalCount; +}; diff --git a/shared/utils/productUtils/priceUtils.ts b/shared/utils/productUtils/priceUtils.ts index 1b3cabbee..33eff4e69 100644 --- a/shared/utils/productUtils/priceUtils.ts +++ b/shared/utils/productUtils/priceUtils.ts @@ -119,7 +119,7 @@ export const isV4Usage = ({ return ( billingType === BillingType.UsageInArrear && - (cusProduct.api_semver === ApiVersion.Beta || + (cusProduct.api_semver === ApiVersion.V1_Beta || notNullish(cusProduct.internal_entity_id)) ); };