refactor: removed transaction in track, added rate limiting
This commit is contained in:
158
bun.lock
158
bun.lock
@@ -63,10 +63,13 @@
|
||||
"@opentelemetry/sdk-trace-node": "^2.0.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||
"@react-email/components": "^0.0.42",
|
||||
"@sentry/bun": "catalog:",
|
||||
"@supabase/supabase-js": "^2.46.2",
|
||||
"@types/qs": "^6.14.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20251114.1",
|
||||
"@upstash/ratelimit": "^2.0.7",
|
||||
"@upstash/redis": "^1.35.6",
|
||||
"@vercel/sdk": "^1.17.0",
|
||||
"ai": "^4.3.10",
|
||||
"autumn-js": "^0.1.8",
|
||||
@@ -197,6 +200,8 @@
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@sentry/react": "^10.25.0",
|
||||
"@sentry/vite-plugin": "^4.6.0",
|
||||
"@squircle/core": "^1.0.6",
|
||||
"@squircle/paint-polyfill": "^1.0.6",
|
||||
"@squircle/tailwindcss": "^1.0.6",
|
||||
@@ -272,6 +277,7 @@
|
||||
},
|
||||
},
|
||||
"catalog": {
|
||||
"@sentry/bun": "10.25.0",
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"drizzle-orm": "0.43.1",
|
||||
"stripe": "19.3.0-beta.1",
|
||||
@@ -345,6 +351,10 @@
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.8.2", "", {}, "sha512-YRjJjNq5KFSjDUoqu5pFUWrrsvGOxl6c3bu+uMFc9HNNptZ2rNU/TI2nLw4jnhQNtka972Ee2m3uqbvDQtPeCA=="],
|
||||
|
||||
"@apm-js-collab/tracing-hooks": ["@apm-js-collab/tracing-hooks@0.3.1", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.8.0", "debug": "^4.4.1", "module-details-from-path": "^1.0.4" } }, "sha512-Vu1CbmPURlN5fTboVuKMoJjbO5qcq9fA5YXpskx3dXe/zTBvjODFoerw+69rVBlRLrJpwPqSDqEuJDEKIrTldw=="],
|
||||
|
||||
"@autumn/scripts": ["@autumn/scripts@workspace:scripts"],
|
||||
|
||||
"@autumn/server": ["@autumn/server@workspace:server"],
|
||||
@@ -973,6 +983,8 @@
|
||||
|
||||
"@posthog/core": ["@posthog/core@1.5.2", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-iedUP3EnOPPxTA2VaIrsrd29lSZnUV+ZrMnvY56timRVeZAXoYCkmjfIs3KBAsF8OUT5h1GXLSkoQdrV0r31OQ=="],
|
||||
|
||||
"@prisma/instrumentation": ["@prisma/instrumentation@6.15.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" }, "peerDependencies": { "@opentelemetry/api": "^1.8" } }, "sha512-6TXaH6OmDkMOQvOxwLZ8XS51hU2v4A3vmE2pSijCIiGRJYyNeMcL6nMHQMyYdZRD8wl7LF3Wzc+AMPMV/9Oo7A=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
@@ -1177,6 +1189,52 @@
|
||||
|
||||
"@selderee/plugin-htmlparser2": ["@selderee/plugin-htmlparser2@0.11.0", "", { "dependencies": { "domhandler": "^5.0.3", "selderee": "^0.11.0" } }, "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ=="],
|
||||
|
||||
"@sentry-internal/browser-utils": ["@sentry-internal/browser-utils@10.25.0", "", { "dependencies": { "@sentry/core": "10.25.0" } }, "sha512-wzg1ITZxrRtQouHPCgpt3tl1GiNAWFVy2RYK2KstFEhpYBAOUn9BAdP7KU9UyHBFKqbAvV4oGtAT8H2/Y4+leA=="],
|
||||
|
||||
"@sentry-internal/feedback": ["@sentry-internal/feedback@10.25.0", "", { "dependencies": { "@sentry/core": "10.25.0" } }, "sha512-qlbT4tOd+WRyKpLdsbi26rkynGBoVabnY8/9rFnTxZ0WIUG5EFhJFqEeRLMyv+uk0uRFF3H0I9+u+qP/BKxIcQ=="],
|
||||
|
||||
"@sentry-internal/replay": ["@sentry-internal/replay@10.25.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.25.0", "@sentry/core": "10.25.0" } }, "sha512-V/kKQn9T46HBTiP0bIThmpVr94K4vXwYM3/EHVpGSq4P9RynX06cgps8GLHq94+A0kX/DbK9igEMZmIuzS1q3A=="],
|
||||
|
||||
"@sentry-internal/replay-canvas": ["@sentry-internal/replay-canvas@10.25.0", "", { "dependencies": { "@sentry-internal/replay": "10.25.0", "@sentry/core": "10.25.0" } }, "sha512-zuj5jVNswZ/aA1nPPbU+VIFkQG0695lbyIfS1Skq+5o2FdRIS3MGnBXw1abI9h4pft8GLQWcKiBxISM7UpSz6w=="],
|
||||
|
||||
"@sentry/babel-plugin-component-annotate": ["@sentry/babel-plugin-component-annotate@4.6.0", "", {}, "sha512-3soTX50JPQQ51FSbb4qvNBf4z/yP7jTdn43vMTp9E4IxvJ9HKJR7OEuKkCMszrZmWsVABXl02msqO7QisePdiQ=="],
|
||||
|
||||
"@sentry/browser": ["@sentry/browser@10.25.0", "", { "dependencies": { "@sentry-internal/browser-utils": "10.25.0", "@sentry-internal/feedback": "10.25.0", "@sentry-internal/replay": "10.25.0", "@sentry-internal/replay-canvas": "10.25.0", "@sentry/core": "10.25.0" } }, "sha512-UgSVT3RTM3vsK914TPuHVJQsjq5ooXVmjMtsWP3Ep+6f7N+1UVX4ZXsyyj5lDOcWdc79FgproD+MrEf9Cj6uBg=="],
|
||||
|
||||
"@sentry/bun": ["@sentry/bun@10.25.0", "", { "dependencies": { "@sentry/core": "10.25.0", "@sentry/node": "10.25.0" } }, "sha512-mcd5wGCI4Y30LrN3ihfDIuEi7DwGAIrEjLz2zN+Ek4equIeAC8kJrNvXl4rBYwoODSkth2K+9tqUPHoX6D4rKw=="],
|
||||
|
||||
"@sentry/bundler-plugin-core": ["@sentry/bundler-plugin-core@4.6.0", "", { "dependencies": { "@babel/core": "^7.18.5", "@sentry/babel-plugin-component-annotate": "4.6.0", "@sentry/cli": "^2.57.0", "dotenv": "^16.3.1", "find-up": "^5.0.0", "glob": "^9.3.2", "magic-string": "0.30.8", "unplugin": "1.0.1" } }, "sha512-Fub2XQqrS258jjS8qAxLLU1k1h5UCNJ76i8m4qZJJdogWWaF8t00KnnTyp9TEDJzrVD64tRXS8+HHENxmeUo3g=="],
|
||||
|
||||
"@sentry/cli": ["@sentry/cli@2.58.2", "", { "dependencies": { "https-proxy-agent": "^5.0.0", "node-fetch": "^2.6.7", "progress": "^2.0.3", "proxy-from-env": "^1.1.0", "which": "^2.0.2" }, "optionalDependencies": { "@sentry/cli-darwin": "2.58.2", "@sentry/cli-linux-arm": "2.58.2", "@sentry/cli-linux-arm64": "2.58.2", "@sentry/cli-linux-i686": "2.58.2", "@sentry/cli-linux-x64": "2.58.2", "@sentry/cli-win32-arm64": "2.58.2", "@sentry/cli-win32-i686": "2.58.2", "@sentry/cli-win32-x64": "2.58.2" }, "bin": { "sentry-cli": "bin/sentry-cli" } }, "sha512-U4u62V4vaTWF+o40Mih8aOpQKqKUbZQt9A3LorIJwaE3tO3XFLRI70eWtW2se1Qmy0RZ74zB14nYcFNFl2t4Rw=="],
|
||||
|
||||
"@sentry/cli-darwin": ["@sentry/cli-darwin@2.58.2", "", { "os": "darwin" }, "sha512-MArsb3zLhA2/cbd4rTm09SmTpnEuZCoZOpuZYkrpDw1qzBVJmRFA1W1hGAQ9puzBIk/ubY3EUhhzuU3zN2uD6w=="],
|
||||
|
||||
"@sentry/cli-linux-arm": ["@sentry/cli-linux-arm@2.58.2", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm" }, "sha512-HU9lTCzcHqCz/7Mt5n+cv+nFuJdc1hGD2h35Uo92GgxX3/IujNvOUfF+nMX9j6BXH6hUt73R5c0Ycq9+a3Parg=="],
|
||||
|
||||
"@sentry/cli-linux-arm64": ["@sentry/cli-linux-arm64@2.58.2", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "arm64" }, "sha512-ay3OeObnbbPrt45cjeUyQjsx5ain1laj1tRszWj37NkKu55NZSp4QCg1gGBZ0gBGhckI9nInEsmKtix00alw2g=="],
|
||||
|
||||
"@sentry/cli-linux-i686": ["@sentry/cli-linux-i686@2.58.2", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "ia32" }, "sha512-CN9p0nfDFsAT1tTGBbzOUGkIllwS3hygOUyTK7LIm9z+UHw5uNgNVqdM/3Vg+02ymjkjISNB3/+mqEM5osGXdA=="],
|
||||
|
||||
"@sentry/cli-linux-x64": ["@sentry/cli-linux-x64@2.58.2", "", { "os": [ "linux", "android", "freebsd", ], "cpu": "x64" }, "sha512-oX/LLfvWaJO50oBVOn4ZvG2SDWPq0MN8SV9eg5tt2nviq+Ryltfr7Rtoo+HfV+eyOlx1/ZXhq9Wm7OT3cQuz+A=="],
|
||||
|
||||
"@sentry/cli-win32-arm64": ["@sentry/cli-win32-arm64@2.58.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-+cl3x2HPVMpoSVGVM1IDWlAEREZrrVQj4xBb0TRKII7g3hUxRsAIcsrr7+tSkie++0FuH4go/b5fGAv51OEF3w=="],
|
||||
|
||||
"@sentry/cli-win32-i686": ["@sentry/cli-win32-i686@2.58.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-omFVr0FhzJ8oTJSg1Kf+gjLgzpYklY0XPfLxZ5iiMiYUKwF5uo1RJRdkUOiEAv0IqpUKnmKcmVCLaDxsWclB7Q=="],
|
||||
|
||||
"@sentry/cli-win32-x64": ["@sentry/cli-win32-x64@2.58.2", "", { "os": "win32", "cpu": "x64" }, "sha512-2NAFs9UxVbRztQbgJSP5i8TB9eJQ7xraciwj/93djrSMHSEbJ0vC47TME0iifgvhlHMs5vqETOKJtfbbpQAQFA=="],
|
||||
|
||||
"@sentry/core": ["@sentry/core@10.25.0", "", {}, "sha512-mGi4BYIPwZjWdOXHrPoXz1AW4/cQbFoiuW/m+OOATmtSoGTDnWYwP+qZU7VLlL+v8ZEzxfPi2C1NPfJtPj7QWA=="],
|
||||
|
||||
"@sentry/node": ["@sentry/node@10.25.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.1.0", "@opentelemetry/core": "^2.1.0", "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/instrumentation-amqplib": "0.51.0", "@opentelemetry/instrumentation-connect": "0.48.0", "@opentelemetry/instrumentation-dataloader": "0.22.0", "@opentelemetry/instrumentation-express": "0.53.0", "@opentelemetry/instrumentation-fs": "0.24.0", "@opentelemetry/instrumentation-generic-pool": "0.48.0", "@opentelemetry/instrumentation-graphql": "0.52.0", "@opentelemetry/instrumentation-hapi": "0.51.0", "@opentelemetry/instrumentation-http": "0.204.0", "@opentelemetry/instrumentation-ioredis": "0.52.0", "@opentelemetry/instrumentation-kafkajs": "0.14.0", "@opentelemetry/instrumentation-knex": "0.49.0", "@opentelemetry/instrumentation-koa": "0.52.0", "@opentelemetry/instrumentation-lru-memoizer": "0.49.0", "@opentelemetry/instrumentation-mongodb": "0.57.0", "@opentelemetry/instrumentation-mongoose": "0.51.0", "@opentelemetry/instrumentation-mysql": "0.50.0", "@opentelemetry/instrumentation-mysql2": "0.51.0", "@opentelemetry/instrumentation-pg": "0.57.0", "@opentelemetry/instrumentation-redis": "0.53.0", "@opentelemetry/instrumentation-tedious": "0.23.0", "@opentelemetry/instrumentation-undici": "0.15.0", "@opentelemetry/resources": "^2.1.0", "@opentelemetry/sdk-trace-base": "^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0", "@prisma/instrumentation": "6.15.0", "@sentry/core": "10.25.0", "@sentry/node-core": "10.25.0", "@sentry/opentelemetry": "10.25.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" } }, "sha512-++mugiYF8X7CLtpymGN3N4J40SvQVIsVa6K7pURhooT4eX1QXYOBJSaOqvOXk5GN4qed5wETHNBkZuXSO0RARQ=="],
|
||||
|
||||
"@sentry/node-core": ["@sentry/node-core@10.25.0", "", { "dependencies": { "@apm-js-collab/tracing-hooks": "^0.3.1", "@sentry/core": "10.25.0", "@sentry/opentelemetry": "10.25.0", "import-in-the-middle": "^1.14.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0" } }, "sha512-Hk0s7r9pkotZ1yfUc9+XX0ALDQ/bjaYsWF23O2q8Yfc4m8NcQio54ztAmdI+Yf+YiHLpt0x9Hlgwpl3AaRvwIA=="],
|
||||
|
||||
"@sentry/opentelemetry": ["@sentry/opentelemetry@10.25.0", "", { "dependencies": { "@sentry/core": "10.25.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0", "@opentelemetry/core": "^1.30.1 || ^2.1.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0", "@opentelemetry/semantic-conventions": "^1.37.0" } }, "sha512-AWCRzUIzvI+0RHXTmGvVx+MUtyyjwmC6F6d6XCnWhBKWGO52I+ucz1X8INIZxCrK05dpviFpeLZy+pzfgw892g=="],
|
||||
|
||||
"@sentry/react": ["@sentry/react@10.25.0", "", { "dependencies": { "@sentry/browser": "10.25.0", "@sentry/core": "10.25.0", "hoist-non-react-statics": "^3.3.2" }, "peerDependencies": { "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, "sha512-LBQHgyPAFzuy99mEJF8ZF2AOxxJiGAmtu10eQhglhFgfJsU7JJVsee0h+vTSmvHMDtFrIwhZi3i1X5snZ/kzoA=="],
|
||||
|
||||
"@sentry/vite-plugin": ["@sentry/vite-plugin@4.6.0", "", { "dependencies": { "@sentry/bundler-plugin-core": "4.6.0", "unplugin": "1.0.1" } }, "sha512-fMR2d+EHwbzBa0S1fp45SNUTProxmyFBp+DeBWWQOSP9IU6AH6ea2rqrpMAnp/skkcdW4z4LSRrOEpMZ5rWXLw=="],
|
||||
|
||||
"@simplewebauthn/browser": ["@simplewebauthn/browser@13.2.2", "", {}, "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA=="],
|
||||
|
||||
"@simplewebauthn/server": ["@simplewebauthn/server@13.2.2", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.3.10", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-rsa": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", "@peculiar/x509": "^1.13.0" } }, "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA=="],
|
||||
@@ -1475,6 +1533,8 @@
|
||||
|
||||
"@types/serve-static": ["@types/serve-static@1.15.10", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "<1" } }, "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw=="],
|
||||
|
||||
"@types/shimmer": ["@types/shimmer@1.2.0", "", {}, "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg=="],
|
||||
|
||||
"@types/superagent": ["@types/superagent@8.1.9", "", { "dependencies": { "@types/cookiejar": "^2.1.5", "@types/methods": "^1.1.4", "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ=="],
|
||||
|
||||
"@types/tedious": ["@types/tedious@4.0.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw=="],
|
||||
@@ -1527,6 +1587,12 @@
|
||||
|
||||
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
|
||||
|
||||
"@upstash/core-analytics": ["@upstash/core-analytics@0.0.10", "", { "dependencies": { "@upstash/redis": "^1.28.3" } }, "sha512-7qJHGxpQgQr9/vmeS1PktEwvNAF7TI4iJDi8Pu2CFZ9YUGHZH4fOP5TfYlZ4aVxfopnELiE4BS4FBjyK7V1/xQ=="],
|
||||
|
||||
"@upstash/ratelimit": ["@upstash/ratelimit@2.0.7", "", { "dependencies": { "@upstash/core-analytics": "^0.0.10" }, "peerDependencies": { "@upstash/redis": "^1.34.3" } }, "sha512-qNQW4uBPKVk8c4wFGj2S/vfKKQxXx1taSJoSGBN36FeiVBBKHQgsjPbKUijZ9Xu5FyVK+pfiXWKIsQGyoje8Fw=="],
|
||||
|
||||
"@upstash/redis": ["@upstash/redis@1.35.6", "", { "dependencies": { "uncrypto": "^0.1.3" } }, "sha512-aSEIGJgJ7XUfTYvhQcQbq835re7e/BXjs8Janq6Pvr6LlmTZnyqwT97RziZLO/8AVUL037RLXqqiQC6kCt+5pA=="],
|
||||
|
||||
"@vercel/sdk": ["@vercel/sdk@1.17.1", "", { "dependencies": { "zod": "^3.25.0 || ^4.0.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": ">=1.5.0 <1.10.0" }, "optionalPeers": ["@modelcontextprotocol/sdk"], "bin": { "mcp": "bin/mcp-server.js" } }, "sha512-wh4Y+nZAsLM0SoaLkYeHn1trIJklG5uwqTbl2keDQEJFuzcE3fG8RwyiuS+ZWW/BdpfiIwN2xAhSiSlbafTzvw=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="],
|
||||
@@ -2051,6 +2117,8 @@
|
||||
|
||||
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
@@ -2343,7 +2411,7 @@
|
||||
|
||||
"luxon": ["luxon@3.5.0", "", {}, "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
"magic-string": ["magic-string@0.30.8", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ=="],
|
||||
|
||||
"make-error": ["make-error@1.3.6", "", {}, "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="],
|
||||
|
||||
@@ -2737,6 +2805,8 @@
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||
|
||||
"shimmer": ["shimmer@1.2.1", "", {}, "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
@@ -2921,6 +2991,8 @@
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"unplugin": ["unplugin@1.0.1", "", { "dependencies": { "acorn": "^8.8.1", "chokidar": "^3.5.3", "webpack-sources": "^3.2.3", "webpack-virtual-modules": "^0.5.0" } }, "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.4", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A=="],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
@@ -2965,6 +3037,10 @@
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"webpack-sources": ["webpack-sources@3.3.3", "", {}, "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg=="],
|
||||
|
||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.5.0", "", {}, "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
@@ -3013,7 +3089,7 @@
|
||||
|
||||
"zod-openapi": ["zod-openapi@5.4.3", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-6kJ/gJdvHZtuxjYHoMtkl2PixCwRuZ/s79dVkEr7arHvZGXfx7Cvh53X3HfJ5h9FzGelXOXlnyjwfX0sKEPByw=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.0", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ=="],
|
||||
|
||||
"zustand": ["zustand@5.0.8", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw=="],
|
||||
|
||||
@@ -3519,6 +3595,8 @@
|
||||
|
||||
"@paralleldrive/cuid2/@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="],
|
||||
|
||||
"@prisma/instrumentation/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.57.2", "", { "dependencies": { "@opentelemetry/api-logs": "0.57.2", "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg=="],
|
||||
|
||||
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
@@ -3537,10 +3615,64 @@
|
||||
|
||||
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@rollup/plugin-replace/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"@sentry/bundler-plugin-core/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
|
||||
|
||||
"@sentry/cli/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.204.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.204.0", "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-vV5+WSxktzoMP8JoYWKeopChy6G3HKk4UQ2hESCRDUUTZqQ3+nM3u8noVG0LmNfRWwcFBnbZ71GKC7vaYYdJ1g=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-amqplib": ["@opentelemetry/instrumentation-amqplib@0.51.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-XGmjYwjVRktD4agFnWBWQXo9SiYHKBxR6Ag3MLXwtLE4R99N3a08kGKM5SC1qOFKIELcQDGFEFT9ydXMH00Luw=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-connect": ["@opentelemetry/instrumentation-connect@0.48.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.38" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-OMjc3SFL4pC16PeK+tDhwP7MRvDPalYCGSvGqUhX5rASkI2H0RuxZHOWElYeXkV0WP+70Gw6JHWac/2Zqwmhdw=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-dataloader": ["@opentelemetry/instrumentation-dataloader@0.22.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.204.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-bXnTcwtngQsI1CvodFkTemrrRSQjAjZxqHVc+CJZTDnidT0T6wt3jkKhnsjU/Kkkc0lacr6VdRpCu2CUWa0OKw=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-express": ["@opentelemetry/instrumentation-express@0.53.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-r/PBafQmFYRjuxLYEHJ3ze1iBnP2GDA1nXOSS6E02KnYNZAVjj6WcDA1MSthtdAUUK0XnotHvvWM8/qz7DMO5A=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-fs": ["@opentelemetry/instrumentation-fs@0.24.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.204.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-HjIxJ6CBRD770KNVaTdMXIv29Sjz4C1kPCCK5x1Ujpc6SNnLGPqUVyJYZ3LUhhnHAqdbrl83ogVWjCgeT4Q0yw=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-generic-pool": ["@opentelemetry/instrumentation-generic-pool@0.48.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.204.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-TLv/On8pufynNR+pUbpkyvuESVASZZKMlqCm4bBImTpXKTpqXaJJ3o/MUDeMlM91rpen+PEv2SeyOKcHCSlgag=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-graphql": ["@opentelemetry/instrumentation-graphql@0.52.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.204.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3fEJ8jOOMwopvldY16KuzHbRhPk8wSsOTSF0v2psmOCGewh6ad+ZbkTx/xyUK9rUdUMWAxRVU0tFpj4Wx1vkPA=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-hapi": ["@opentelemetry/instrumentation-hapi@0.51.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-qyf27DaFNL1Qhbo/da+04MSCw982B02FhuOS5/UF+PMhM61CcOiu7fPuXj8TvbqyReQuJFljXE6UirlvoT/62g=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-http": ["@opentelemetry/instrumentation-http@0.204.0", "", { "dependencies": { "@opentelemetry/core": "2.1.0", "@opentelemetry/instrumentation": "0.204.0", "@opentelemetry/semantic-conventions": "^1.29.0", "forwarded-parse": "2.1.2" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-1afJYyGRA4OmHTv0FfNTrTAzoEjPQUYgd+8ih/lX0LlZBnGio/O80vxA0lN3knsJPS7FiDrsDrWq25K7oAzbkw=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-ioredis": ["@opentelemetry/instrumentation-ioredis@0.52.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/redis-common": "^0.38.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-rUvlyZwI90HRQPYicxpDGhT8setMrlHKokCtBtZgYxQWRF5RBbG4q0pGtbZvd7kyseuHbFpA3I/5z7M8b/5ywg=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-kafkajs": ["@opentelemetry/instrumentation-kafkajs@0.14.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.30.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kbB5yXS47dTIdO/lfbbXlzhvHFturbux4EpP0+6H78Lk0Bn4QXiZQW7rmZY1xBCY16mNcCb8Yt0mhz85hTnSVA=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-knex": ["@opentelemetry/instrumentation-knex@0.49.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.33.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-NKsRRT27fbIYL4Ix+BjjP8h4YveyKc+2gD6DMZbr5R5rUeDqfC8+DTfIt3c3ex3BIc5Vvek4rqHnN7q34ZetLQ=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-koa": ["@opentelemetry/instrumentation-koa@0.52.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-JJSBYLDx/mNSy8Ibi/uQixu2rH0bZODJa8/cz04hEhRaiZQoeJ5UrOhO/mS87IdgVsHrnBOsZ6vDu09znupyuA=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-lru-memoizer": ["@opentelemetry/instrumentation-lru-memoizer@0.49.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.204.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-ctXu+O/1HSadAxtjoEg2w307Z5iPyLOMM8IRNwjaKrIpNAthYGSOanChbk1kqY6zU5CrpkPHGdAT6jk8dXiMqw=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-mongodb": ["@opentelemetry/instrumentation-mongodb@0.57.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-KD6Rg0KSHWDkik+qjIOWoksi1xqSpix8TSPfquIK1DTmd9OTFb5PHmMkzJe16TAPVEuElUW8gvgP59cacFcrMQ=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-mongoose": ["@opentelemetry/instrumentation-mongoose@0.51.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-gwWaAlhhV2By7XcbyU3DOLMvzsgeaymwP/jktDC+/uPkCmgB61zurwqOQdeiRq9KAf22Y2dtE5ZLXxytJRbEVA=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-mysql": ["@opentelemetry/instrumentation-mysql@0.50.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/mysql": "2.15.27" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-duKAvMRI3vq6u9JwzIipY9zHfikN20bX05sL7GjDeLKr2qV0LQ4ADtKST7KStdGcQ+MTN5wghWbbVdLgNcB3rA=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-mysql2": ["@opentelemetry/instrumentation-mysql2@0.51.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.41.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-zT2Wg22Xn43RyfU3NOUmnFtb5zlDI0fKcijCj9AcK9zuLZ4ModgtLXOyBJSSfO+hsOCZSC1v/Fxwj+nZJFdzLQ=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-pg": ["@opentelemetry/instrumentation-pg@0.57.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.34.0", "@opentelemetry/sql-common": "^0.41.0", "@types/pg": "8.15.5", "@types/pg-pool": "2.0.6" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-dWLGE+r5lBgm2A8SaaSYDE3OKJ/kwwy5WLyGyzor8PLhUL9VnJRiY6qhp4njwhnljiLtzeffRtG2Mf/YyWLeTw=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-redis": ["@opentelemetry/instrumentation-redis@0.53.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/redis-common": "^0.38.0", "@opentelemetry/semantic-conventions": "^1.27.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-WUHV8fr+8yo5RmzyU7D5BIE1zwiaNQcTyZPwtxlfr7px6NYYx7IIpSihJK7WA60npWynfxxK1T67RAVF0Gdfjg=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-tedious": ["@opentelemetry/instrumentation-tedious@0.23.0", "", { "dependencies": { "@opentelemetry/instrumentation": "^0.204.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@types/tedious": "^4.0.14" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-3TMTk/9VtlRonVTaU4tCzbg4YqW+Iq/l5VnN2e5whP6JgEg/PKfrGbqQ+CxQWNLfLaQYIUgEZqAn5gk/inh1uQ=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-undici": ["@opentelemetry/instrumentation-undici@0.15.0", "", { "dependencies": { "@opentelemetry/core": "^2.0.0", "@opentelemetry/instrumentation": "^0.204.0" }, "peerDependencies": { "@opentelemetry/api": "^1.7.0" } }, "sha512-sNFGA/iCDlVkNjzTzPRcudmI11vT/WAfAguRdZY9IspCw02N4WSC72zTuQhSMheh2a1gdeM9my1imnKRvEEvEg=="],
|
||||
|
||||
"@smithy/eventstream-codec/@smithy/types": ["@smithy/types@1.2.0", "", { "dependencies": { "tslib": "^2.5.0" } }, "sha512-z1r00TvBqF3dh4aHhya7nz1HhvCg4TRmw51fjMrh5do3h+ngSstt/yKlNbHeb9QxJmFbmN8KEVSWgb1bRvfEoA=="],
|
||||
|
||||
"@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@1.1.0", "", { "dependencies": { "tslib": "^2.5.0" } }, "sha512-7UtIE9eH0u41zpB60Jzr0oNCQ3hMJUabMcKRUVjmyHTXiWDE4vjSqN6qlih7rCNeKGbioS7f/y2Jgym4QZcKFg=="],
|
||||
|
||||
"@tailwindcss/node/magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
||||
@@ -3697,6 +3829,8 @@
|
||||
|
||||
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
|
||||
|
||||
"unplugin/chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
|
||||
|
||||
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
|
||||
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
|
||||
@@ -4043,6 +4177,20 @@
|
||||
|
||||
"@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.0.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw=="],
|
||||
|
||||
"@prisma/instrumentation/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.57.2", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A=="],
|
||||
|
||||
"@sentry/bundler-plugin-core/glob/minimatch": ["minimatch@8.0.4", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA=="],
|
||||
|
||||
"@sentry/bundler-plugin-core/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
|
||||
|
||||
"@sentry/cli/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.204.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DqxY8yoAaiBPivoJD4UtgrMS8gEmzZ5lnaxzPojzLVHBGqPxgWm4zcuvcUHZiqQ6kRX2Klel2r9y8cA2HAtqpw=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-http/@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=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.15.5", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ=="],
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
@@ -4171,6 +4319,10 @@
|
||||
|
||||
"type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
"unplugin/chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"unplugin/chokidar/readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
|
||||
|
||||
"widest-line/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
@@ -4373,6 +4525,8 @@
|
||||
|
||||
"tsc-alias/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"unplugin/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@4.1.3", "", { "dependencies": { "@smithy/protocol-http": "^4.1.8", "@smithy/querystring-builder": "^3.0.11", "@smithy/types": "^3.7.2", "@smithy/util-base64": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-6SxNltSncI8s689nvnzZQc/dPXcpHQ34KUj6gR/HBroytKOd/isMG3gJF/zBE1TBmTT18TXyzhg3O3SOOqGEhA=="],
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"catalog": {
|
||||
"stripe": "19.3.0-beta.1",
|
||||
"drizzle-orm": "0.43.1",
|
||||
"drizzle-kit": "^0.31.1"
|
||||
"drizzle-kit": "^0.31.1",
|
||||
"@sentry/bun": "10.25.0"
|
||||
}
|
||||
},
|
||||
"type": "module",
|
||||
@@ -20,15 +21,12 @@
|
||||
"vite:build": "bun -F @autumn/vite build:bun",
|
||||
"d": "ENV_FILE=.env infisical run --env=dev -- bun scripts/dev.ts",
|
||||
"p": "ENV_FILE=.env.prod infisical run --env=prod -- bun scripts/dev.ts",
|
||||
|
||||
"setup": "node scripts/setup/setup.js",
|
||||
"setup:test": "bun scripts/setup/setup-test.ts",
|
||||
"tests": "infisical run --env=dev -- bun scripts/test.ts",
|
||||
|
||||
"migrate-functions": "infisical run --env=dev -- bun scripts/migrations/migrate-functions.ts",
|
||||
"migrate-functions:prod": "infisical run --env=prod -- bun scripts/migrations/migrate-functions.ts",
|
||||
"validate-schema": "infisical run --env=prod -- bun scripts/migrations/validate-schema.ts",
|
||||
|
||||
"setupci": "node scripts/setup/setupci.js",
|
||||
"replicate": "bun scripts/db/replicate.ts",
|
||||
"db:push": " bun -F @autumn/shared db:push",
|
||||
|
||||
123
server/experiments/rateLimiting.ts
Normal file
123
server/experiments/rateLimiting.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import AutumnError, { AutumnInt } from "../src/external/autumn/autumnCli";
|
||||
|
||||
export const main = async () => {
|
||||
console.log("🚀 Starting rate limit test...\n");
|
||||
|
||||
// Initialize client
|
||||
const autumn = new AutumnInt();
|
||||
const numRequests = 200;
|
||||
|
||||
const customerId1 = "test1";
|
||||
const customerId2 = "test2";
|
||||
|
||||
console.log(`Testing with ${numRequests} concurrent requests to /track endpoint`);
|
||||
console.log(`Using base URL: ${autumn.baseUrl}\n`);
|
||||
|
||||
// Create all track requests
|
||||
const cusId1Promises = [];
|
||||
const cusId2Promises = [];
|
||||
for (let i = 0; i < numRequests; i++) {
|
||||
cusId1Promises.push(
|
||||
autumn.customers.create({
|
||||
id: customerId1,
|
||||
}),
|
||||
);
|
||||
cusId2Promises.push(
|
||||
autumn.customers.create({
|
||||
id: customerId2,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// Execute all requests concurrently for both customers
|
||||
const startTime = Date.now();
|
||||
const [cusId1Results, cusId2Results] = await Promise.all([
|
||||
Promise.allSettled(cusId1Promises),
|
||||
Promise.allSettled(cusId2Promises),
|
||||
]);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// Helper function to analyze results
|
||||
const analyzeResults = (results: PromiseSettledResult<unknown>[]) => {
|
||||
const succeeded = results.filter((r) => r.status === "fulfilled").length;
|
||||
const rateLimited = results.filter(
|
||||
(r) =>
|
||||
r.status === "rejected" &&
|
||||
r.reason instanceof AutumnError &&
|
||||
r.reason.code === "rate_limit_exceeded",
|
||||
).length;
|
||||
const otherErrors = results.filter(
|
||||
(r) =>
|
||||
r.status === "rejected" &&
|
||||
!(
|
||||
r.reason instanceof AutumnError &&
|
||||
r.reason.code === "rate_limit_exceeded"
|
||||
),
|
||||
).length;
|
||||
return { succeeded, rateLimited, otherErrors };
|
||||
};
|
||||
|
||||
const cus1Stats = analyzeResults(cusId1Results);
|
||||
const cus2Stats = analyzeResults(cusId2Results);
|
||||
const totalRequests = numRequests * 2;
|
||||
|
||||
// Display results
|
||||
console.log("📊 Results:");
|
||||
console.log("═".repeat(60));
|
||||
console.log(`Total requests: ${totalRequests} (${numRequests} per customer)`);
|
||||
console.log(`⏱️ Duration: ${duration}ms`);
|
||||
console.log(`📈 Throughput: ${Math.round(totalRequests / (duration / 1000))} req/s`);
|
||||
console.log("═".repeat(60));
|
||||
|
||||
console.log(`\n👤 Customer 1 (${customerId1}):`);
|
||||
console.log("─".repeat(60));
|
||||
console.log(` Total: ${numRequests}`);
|
||||
console.log(` ✅ Succeeded: ${cus1Stats.succeeded}`);
|
||||
console.log(` ⛔ Rate limited: ${cus1Stats.rateLimited}`);
|
||||
console.log(` ❌ Other errors: ${cus1Stats.otherErrors}`);
|
||||
|
||||
console.log(`\n👤 Customer 2 (${customerId2}):`);
|
||||
console.log("─".repeat(60));
|
||||
console.log(` Total: ${numRequests}`);
|
||||
console.log(` ✅ Succeeded: ${cus2Stats.succeeded}`);
|
||||
console.log(` ⛔ Rate limited: ${cus2Stats.rateLimited}`);
|
||||
console.log(` ❌ Other errors: ${cus2Stats.otherErrors}`);
|
||||
|
||||
console.log("\n📈 Combined Stats:");
|
||||
console.log("─".repeat(60));
|
||||
console.log(
|
||||
` ✅ Total succeeded: ${cus1Stats.succeeded + cus2Stats.succeeded}`,
|
||||
);
|
||||
console.log(
|
||||
` ⛔ Total rate limited: ${cus1Stats.rateLimited + cus2Stats.rateLimited}`,
|
||||
);
|
||||
console.log(
|
||||
` ❌ Total errors: ${cus1Stats.otherErrors + cus2Stats.otherErrors}`,
|
||||
);
|
||||
|
||||
// Show sample errors if any
|
||||
const totalErrors = cus1Stats.otherErrors + cus2Stats.otherErrors;
|
||||
if (totalErrors > 0) {
|
||||
console.log("\n⚠️ Sample of other errors:");
|
||||
const errorSamples = [...cusId1Results, ...cusId2Results]
|
||||
.filter(
|
||||
(r) =>
|
||||
r.status === "rejected" &&
|
||||
!(
|
||||
r.reason instanceof AutumnError &&
|
||||
r.reason.code === "rate_limit_exceeded"
|
||||
),
|
||||
)
|
||||
.slice(0, 3);
|
||||
|
||||
for (const sample of errorSamples) {
|
||||
if (sample.status === "rejected") {
|
||||
console.log(` - ${sample.reason}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("\n✨ Test complete!");
|
||||
};
|
||||
|
||||
await main();
|
||||
@@ -12,12 +12,10 @@
|
||||
"c": "ENV_FILE=.env infisical run --env=dev -- bun cron",
|
||||
"dev": "cross-env NODE_ENV=development bunx nodemon",
|
||||
"workers:dev": "cross-env NODE_ENV=development bunx nodemon --exec bun src/workers.ts --signal SIGTERM --delay 500ms",
|
||||
|
||||
"start": "bun src/index.ts",
|
||||
"workers": "bun src/workers.ts",
|
||||
"cron": "bun src/cron.ts",
|
||||
"check": "bun src/check.ts",
|
||||
|
||||
"t": "ENV_FILE=.env infisical run --env=dev -- bun tests/testRunner/runParallelGroupsV3.ts",
|
||||
"parallel-tests": "ENV_FILE=.env infisical run --env=dev -- bun tests/testRunner/runParallelGroupsV3.ts",
|
||||
"parallel-tests:v1": "ENV_FILE=.env infisical run --env=dev -- bun tests/testRunner/runParallelGroups.ts",
|
||||
@@ -57,10 +55,13 @@
|
||||
"@opentelemetry/sdk-trace-node": "^2.0.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||
"@react-email/components": "^0.0.42",
|
||||
"@sentry/bun": "catalog:",
|
||||
"@supabase/supabase-js": "^2.46.2",
|
||||
"@types/qs": "^6.14.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20251114.1",
|
||||
"@upstash/ratelimit": "^2.0.7",
|
||||
"@upstash/redis": "^1.35.6",
|
||||
"@vercel/sdk": "^1.17.0",
|
||||
"ai": "^4.3.10",
|
||||
"autumn-js": "^0.1.8",
|
||||
|
||||
@@ -325,8 +325,9 @@ end
|
||||
-- cusFeature: Balance object to deduct from
|
||||
-- amount: Amount to handle (positive for deduction, negative for refund)
|
||||
-- adjustGrantedBalance: If true, decrement granted_balance instead of incrementing usage
|
||||
-- overageBehavior: "cap" or "reject" - affects allocatedFeatureBypass logic
|
||||
-- Returns: { remaining: number, deltas: [{key, field, delta}], stateChanges: [{type, index, field, newValue/delta}] }
|
||||
local function deductFromOverage(cusFeature, amount, adjustGrantedBalance)
|
||||
local function deductFromOverage(cusFeature, amount, adjustGrantedBalance, overageBehavior)
|
||||
local remaining = amount
|
||||
local deltas = {}
|
||||
local stateChanges = {}
|
||||
@@ -350,8 +351,11 @@ local function deductFromOverage(cusFeature, amount, adjustGrantedBalance)
|
||||
end
|
||||
|
||||
-- Check if overage is allowed
|
||||
-- Continuous use features automatically allow overage
|
||||
local allowOverage = cusFeature.overage_allowed or (cusFeature.feature and cusFeature.feature.type == "metered" and cusFeature.feature.consumable == false)
|
||||
-- Continuous use features automatically allow overage (unless overageBehavior is "reject")
|
||||
local allocatedFeatureBypass = cusFeature.feature and cusFeature.feature.type == "metered" and cusFeature.feature.consumable == false and overageBehavior ~= "reject"
|
||||
local allowOverage = cusFeature.overage_allowed or allocatedFeatureBypass
|
||||
|
||||
|
||||
|
||||
if not allowOverage then
|
||||
return {
|
||||
@@ -553,8 +557,9 @@ end
|
||||
-- cusFeature: Balance object to deduct from
|
||||
-- amount: Amount to deduct (can be negative for refunds)
|
||||
-- adjustGrantedBalance: If true, decrement granted_balance instead of incrementing usage
|
||||
-- overageBehavior: "cap" or "reject" - passed to deductFromOverage
|
||||
-- Returns: { remaining: number, deltas: [{key, field, delta}], stateChanges: [{type, index, field, newValue/delta}] }
|
||||
local function deductFromMainBalance(cusFeature, amount, adjustGrantedBalance)
|
||||
local function deductFromMainBalance(cusFeature, amount, adjustGrantedBalance, overageBehavior)
|
||||
local allDeltas = {}
|
||||
local allStateChanges = {}
|
||||
local remaining = amount
|
||||
@@ -574,7 +579,7 @@ local function deductFromMainBalance(cusFeature, amount, adjustGrantedBalance)
|
||||
|
||||
-- Pass 2: Deduct from overage (increments purchased_balance up to max_purchase)
|
||||
if remaining > 0 then
|
||||
local overageResult = deductFromOverage(cusFeature, remaining, adjustGrantedBalance)
|
||||
local overageResult = deductFromOverage(cusFeature, remaining, adjustGrantedBalance, overageBehavior)
|
||||
remaining = overageResult.remaining
|
||||
|
||||
for _, delta in ipairs(overageResult.deltas) do
|
||||
@@ -587,7 +592,7 @@ local function deductFromMainBalance(cusFeature, amount, adjustGrantedBalance)
|
||||
-- NEGATIVE AMOUNT (REFUND): overage → current_balance
|
||||
else
|
||||
-- Pass 1: Refund from overage (decrements purchased_balance down to 0)
|
||||
local overageResult = deductFromOverage(cusFeature, remaining, adjustGrantedBalance)
|
||||
local overageResult = deductFromOverage(cusFeature, remaining, adjustGrantedBalance, overageBehavior)
|
||||
remaining = overageResult.remaining
|
||||
|
||||
for _, delta in ipairs(overageResult.deltas) do
|
||||
@@ -626,7 +631,7 @@ end
|
||||
|
||||
-- Deduct from a single customer feature (handles rollovers + main balance)
|
||||
-- Returns: { remaining: number, deltas: array, stateChanges: array }
|
||||
local function deductFromCusFeature(cusFeature, amount)
|
||||
local function deductFromCusFeature(cusFeature, amount, overageBehavior)
|
||||
local allDeltas = {}
|
||||
local allStateChanges = {}
|
||||
|
||||
@@ -644,7 +649,7 @@ local function deductFromCusFeature(cusFeature, amount)
|
||||
|
||||
-- Step 2: Deduct remaining from main balance
|
||||
if remaining ~= 0 then
|
||||
local mainResult = deductFromMainBalance(cusFeature, remaining, adjustGrantedBalance)
|
||||
local mainResult = deductFromMainBalance(cusFeature, remaining, adjustGrantedBalance, overageBehavior)
|
||||
remaining = mainResult.remaining
|
||||
|
||||
-- Collect main balance deltas and state changes
|
||||
@@ -668,7 +673,7 @@ end
|
||||
-- If targetEntityId is provided, only deduct from that entity (entity-level tracking)
|
||||
-- If targetEntityId is nil, deduct from ALL entities (customer-level tracking)
|
||||
-- Returns: { remaining: number, deltas: array, customerStateChanges: array, entityStateChanges: { [entityId] = array } }
|
||||
local function deductFromFeatureWithEntities(customerFeature, entityFeaturesMap, amount, targetEntityId)
|
||||
local function deductFromFeatureWithEntities(customerFeature, entityFeaturesMap, amount, targetEntityId, overageBehavior)
|
||||
local allDeltas = {}
|
||||
local customerStateChanges = {}
|
||||
local entityStateChanges = {}
|
||||
@@ -701,7 +706,7 @@ local function deductFromFeatureWithEntities(customerFeature, entityFeaturesMap,
|
||||
if entityFeatures then
|
||||
local entityFeature = entityFeatures[customerFeature.id]
|
||||
if entityFeature and remaining ~= 0 then
|
||||
local entityMainResult = deductFromMainBalance(entityFeature, remaining, adjustGrantedBalance)
|
||||
local entityMainResult = deductFromMainBalance(entityFeature, remaining, adjustGrantedBalance, overageBehavior)
|
||||
remaining = entityMainResult.remaining
|
||||
for _, delta in ipairs(entityMainResult.deltas) do
|
||||
table.insert(allDeltas, delta)
|
||||
@@ -729,7 +734,7 @@ local function deductFromFeatureWithEntities(customerFeature, entityFeaturesMap,
|
||||
|
||||
-- Step 4: Deduct from customer main balance
|
||||
if remaining ~= 0 then
|
||||
local customerMainResult = deductFromMainBalance(customerFeature, remaining, adjustGrantedBalance)
|
||||
local customerMainResult = deductFromMainBalance(customerFeature, remaining, adjustGrantedBalance, overageBehavior)
|
||||
remaining = customerMainResult.remaining
|
||||
for _, delta in ipairs(customerMainResult.deltas) do
|
||||
table.insert(allDeltas, delta)
|
||||
@@ -753,7 +758,7 @@ local function deductFromFeatureWithEntities(customerFeature, entityFeaturesMap,
|
||||
|
||||
-- Step 2: Deduct from customer main balance
|
||||
if remaining ~= 0 then
|
||||
local customerMainResult = deductFromMainBalance(customerFeature, remaining, adjustGrantedBalance)
|
||||
local customerMainResult = deductFromMainBalance(customerFeature, remaining, adjustGrantedBalance, overageBehavior)
|
||||
remaining = customerMainResult.remaining
|
||||
for _, delta in ipairs(customerMainResult.deltas) do
|
||||
table.insert(allDeltas, delta)
|
||||
@@ -802,7 +807,7 @@ local function deductFromFeatureWithEntities(customerFeature, entityFeaturesMap,
|
||||
local entityFeatures = entityFeaturesMap[entityId]
|
||||
local entityFeature = entityFeatures[customerFeature.id]
|
||||
if entityFeature and remaining ~= 0 then
|
||||
local entityMainResult = deductFromMainBalance(entityFeature, remaining, adjustGrantedBalance)
|
||||
local entityMainResult = deductFromMainBalance(entityFeature, remaining, adjustGrantedBalance, overageBehavior)
|
||||
remaining = entityMainResult.remaining
|
||||
for _, delta in ipairs(entityMainResult.deltas) do
|
||||
table.insert(allDeltas, delta)
|
||||
@@ -901,6 +906,7 @@ local function processRequest(request, loadedCusFeatures, entityFeatureStates)
|
||||
local syncMode = request.syncMode or false
|
||||
local targetBalance = request.targetBalance
|
||||
|
||||
|
||||
-- Collect all deltas and state changes for this request
|
||||
local requestDeltas = {}
|
||||
local requestStateChanges = {}
|
||||
@@ -938,7 +944,7 @@ local function processRequest(request, loadedCusFeatures, entityFeatureStates)
|
||||
if cusFeature then
|
||||
-- Customer has this feature - deduct from customer + entities
|
||||
if not cusFeature.unlimited then
|
||||
local result = deductFromFeatureWithEntities(cusFeature, entityFeatureStates, amount, entityId)
|
||||
local result = deductFromFeatureWithEntities(cusFeature, entityFeatureStates, amount, entityId, overageBehavior)
|
||||
|
||||
-- Collect deltas
|
||||
for _, delta in ipairs(result.deltas) do
|
||||
@@ -977,7 +983,7 @@ local function processRequest(request, loadedCusFeatures, entityFeatureStates)
|
||||
if entityFeatures and entityFeatures[featureId] then
|
||||
local entityFeature = entityFeatures[featureId]
|
||||
if not entityFeature.unlimited then
|
||||
local result = deductFromCusFeature(entityFeature, amount)
|
||||
local result = deductFromCusFeature(entityFeature, amount, overageBehavior)
|
||||
|
||||
-- Collect deltas
|
||||
for _, delta in ipairs(result.deltas) do
|
||||
@@ -1011,7 +1017,7 @@ local function processRequest(request, loadedCusFeatures, entityFeatureStates)
|
||||
local entityFeature = entityFeatures[featureId]
|
||||
if entityFeature and remainingAmount ~= 0 then
|
||||
if not entityFeature.unlimited then
|
||||
local result = deductFromCusFeature(entityFeature, remainingAmount)
|
||||
local result = deductFromCusFeature(entityFeature, remainingAmount, overageBehavior)
|
||||
|
||||
-- Collect deltas
|
||||
for _, delta in ipairs(result.deltas) do
|
||||
@@ -1050,7 +1056,7 @@ local function processRequest(request, loadedCusFeatures, entityFeatureStates)
|
||||
local creditAmount = remainingAmount * creditItem.credit_cost
|
||||
|
||||
if not otherCusFeature.unlimited then
|
||||
local result = deductFromFeatureWithEntities(otherCusFeature, entityFeatureStates, creditAmount, entityId)
|
||||
local result = deductFromFeatureWithEntities(otherCusFeature, entityFeatureStates, creditAmount, entityId, overageBehavior)
|
||||
|
||||
-- Collect deltas
|
||||
for _, delta in ipairs(result.deltas) do
|
||||
|
||||
38
server/src/external/autumn/autumnCli.ts
vendored
38
server/src/external/autumn/autumnCli.ts
vendored
@@ -91,6 +91,32 @@ export class AutumnInt {
|
||||
const response = await fetch(`${this.baseUrl}${path}`, {
|
||||
headers: this.headers,
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
// Handle rate limit errors
|
||||
if (response.status === 429) {
|
||||
throw new AutumnError({
|
||||
message: `request failed, rate limit exceeded`,
|
||||
code: "rate_limit_exceeded",
|
||||
});
|
||||
}
|
||||
|
||||
let error: any;
|
||||
try {
|
||||
error = await response.json();
|
||||
} catch (error) {
|
||||
throw new AutumnError({
|
||||
message: `request failed, error: ${error}`,
|
||||
code: ErrCode.InternalError,
|
||||
});
|
||||
}
|
||||
|
||||
throw new AutumnError({
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
});
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
@@ -582,8 +608,16 @@ export class AutumnInt {
|
||||
},
|
||||
};
|
||||
|
||||
track = async (params: TrackParams) => {
|
||||
const data = await this.post(`/track`, params);
|
||||
track = async (
|
||||
params: TrackParams,
|
||||
{ skipCache = false }: { skipCache?: boolean } = {},
|
||||
) => {
|
||||
const queryParams = new URLSearchParams();
|
||||
if (skipCache) {
|
||||
queryParams.append("skip_cache", "true");
|
||||
}
|
||||
|
||||
const data = await this.post(`/track?${queryParams.toString()}`, params);
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
48
server/src/external/upstash/initUpstash.ts
vendored
Normal file
48
server/src/external/upstash/initUpstash.ts
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Ratelimit } from "@upstash/ratelimit";
|
||||
import { Redis } from "@upstash/redis";
|
||||
import {
|
||||
CHECK_RATE_LIMIT,
|
||||
GENERAL_RATE_LIMIT,
|
||||
TRACK_RATE_LIMIT,
|
||||
} from "./rateLimitConstants";
|
||||
|
||||
let upstash: Redis | undefined;
|
||||
let generalRateLimiter: Ratelimit | undefined;
|
||||
let trackRateLimiter: Ratelimit | undefined;
|
||||
let checkRateLimiter: Ratelimit | undefined;
|
||||
|
||||
if (
|
||||
process.env.UPSTASH_REDIS_REST_URL &&
|
||||
process.env.UPSTASH_REDIS_REST_TOKEN
|
||||
) {
|
||||
upstash = new Redis({
|
||||
url: process.env.UPSTASH_REDIS_REST_URL ?? "",
|
||||
token: process.env.UPSTASH_REDIS_REST_TOKEN ?? "",
|
||||
});
|
||||
|
||||
// 1. General rate limiter
|
||||
generalRateLimiter = new Ratelimit({
|
||||
redis: upstash,
|
||||
limiter: Ratelimit.slidingWindow(GENERAL_RATE_LIMIT, "1s"),
|
||||
});
|
||||
|
||||
// 2. Track rate limiter
|
||||
trackRateLimiter = new Ratelimit({
|
||||
redis: upstash,
|
||||
limiter: Ratelimit.slidingWindow(TRACK_RATE_LIMIT, "1s"),
|
||||
});
|
||||
|
||||
// 3. Check / get customer rate limiter
|
||||
checkRateLimiter = new Ratelimit({
|
||||
redis: upstash,
|
||||
limiter: Ratelimit.slidingWindow(CHECK_RATE_LIMIT, "1s"),
|
||||
});
|
||||
}
|
||||
|
||||
export enum RateLimitType {
|
||||
General = "general",
|
||||
Track = "track",
|
||||
Check = "check",
|
||||
}
|
||||
|
||||
export { upstash, generalRateLimiter, trackRateLimiter, checkRateLimiter };
|
||||
7
server/src/external/upstash/rateLimitConstants.ts
vendored
Normal file
7
server/src/external/upstash/rateLimitConstants.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
export const GENERAL_RATE_LIMIT = 1000; // per org
|
||||
export const TRACK_RATE_LIMIT = 10000; // per customer ID
|
||||
export const CHECK_RATE_LIMIT = 10000; // per customer ID
|
||||
|
||||
// const TRACK_RATE_LIMIT = 10;
|
||||
// const CHECK_RATE_LIMIT = 10;
|
||||
// const GENERAL_RATE_LIMIT = 10;
|
||||
124
server/src/external/upstash/rateLimitUtils.ts
vendored
Normal file
124
server/src/external/upstash/rateLimitUtils.ts
vendored
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { Context } from "hono";
|
||||
import {
|
||||
parseCustomerIdFromBody,
|
||||
parseCustomerIdFromUrl,
|
||||
} from "../../honoMiddlewares/analyticsMiddleware";
|
||||
import { matchRoute } from "../../honoMiddlewares/middlewareUtils";
|
||||
import type { HonoEnv } from "../../honoUtils/HonoEnv";
|
||||
import {
|
||||
checkRateLimiter,
|
||||
generalRateLimiter,
|
||||
RateLimitType,
|
||||
trackRateLimiter,
|
||||
} from "./initUpstash";
|
||||
|
||||
export const getRateLimitType = (c: Context<HonoEnv>) => {
|
||||
const method = c.req.method;
|
||||
const path = c.req.path;
|
||||
|
||||
// Exact match patterns for track endpoints
|
||||
const trackPatterns = [
|
||||
{
|
||||
method: "POST",
|
||||
url: "/v1/events",
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
url: "/v1/track",
|
||||
},
|
||||
];
|
||||
|
||||
// Patterns for check endpoints (including dynamic customer_id)
|
||||
const checkPatterns = [
|
||||
{
|
||||
method: "POST",
|
||||
url: "/v1/check",
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
url: "/v1/entitled",
|
||||
},
|
||||
];
|
||||
|
||||
const getCustomerPatterns = [
|
||||
{
|
||||
method: "GET",
|
||||
url: "/v1/customers/:customer_id",
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
url: "/v1/customers",
|
||||
},
|
||||
];
|
||||
|
||||
if (
|
||||
trackPatterns.some((pattern) => matchRoute({ url: path, method, pattern }))
|
||||
) {
|
||||
return RateLimitType.Track;
|
||||
}
|
||||
|
||||
if (
|
||||
checkPatterns.some((pattern) =>
|
||||
matchRoute({ url: path, method, pattern }),
|
||||
) ||
|
||||
getCustomerPatterns.some((pattern) =>
|
||||
matchRoute({ url: path, method, pattern }),
|
||||
)
|
||||
) {
|
||||
return RateLimitType.Check;
|
||||
}
|
||||
|
||||
return RateLimitType.General;
|
||||
};
|
||||
|
||||
export const getRateLimiter = ({
|
||||
rateLimitType,
|
||||
}: {
|
||||
rateLimitType: RateLimitType;
|
||||
}) => {
|
||||
switch (rateLimitType) {
|
||||
case RateLimitType.Track:
|
||||
return trackRateLimiter;
|
||||
case RateLimitType.Check:
|
||||
return checkRateLimiter;
|
||||
case RateLimitType.General:
|
||||
return generalRateLimiter;
|
||||
}
|
||||
};
|
||||
|
||||
export const getRateLimitKey = async ({
|
||||
c,
|
||||
rateLimitType,
|
||||
}: {
|
||||
c: Context<HonoEnv>;
|
||||
rateLimitType: RateLimitType;
|
||||
}) => {
|
||||
const ctx = c.get("ctx");
|
||||
const orgId = ctx.org?.id;
|
||||
const env = ctx.env;
|
||||
// 1. If rate limit type is general
|
||||
switch (rateLimitType) {
|
||||
case RateLimitType.Track: {
|
||||
const res = await parseCustomerIdFromBody(c);
|
||||
const customerId = res?.customerId;
|
||||
return `track:${orgId}:${env}:${customerId}`;
|
||||
}
|
||||
|
||||
case RateLimitType.Check: {
|
||||
const res = await parseCustomerIdFromBody(c);
|
||||
const urlCustomerId = parseCustomerIdFromUrl({ url: c.req.path });
|
||||
|
||||
const customerId = res?.customerId || urlCustomerId;
|
||||
const sendEvent = res?.sendEvent;
|
||||
|
||||
if (customerId && sendEvent) {
|
||||
return `track:${orgId}:${env}:${customerId}`;
|
||||
}
|
||||
|
||||
return `check:${orgId}:${env}:${customerId}`;
|
||||
}
|
||||
|
||||
case RateLimitType.General:
|
||||
return `general:${orgId}:${env}`;
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as Sentry from "@sentry/bun";
|
||||
import type { Context, Next } from "hono";
|
||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
|
||||
const parseCustomerIdFromUrl = ({
|
||||
export const parseCustomerIdFromUrl = ({
|
||||
url,
|
||||
}: {
|
||||
url: string;
|
||||
@@ -21,6 +22,33 @@ const parseCustomerIdFromUrl = ({
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const parseCustomerIdFromBody = async (
|
||||
c: Context<HonoEnv>,
|
||||
): Promise<
|
||||
{ customerId: string | undefined; sendEvent: boolean | undefined } | undefined
|
||||
> => {
|
||||
const method = c.req.method;
|
||||
if (method === "POST" || method === "PUT" || method === "PATCH") {
|
||||
try {
|
||||
// Clone the request to read body without consuming it
|
||||
const body = await c.req.json();
|
||||
|
||||
const isCreateCustomerPath =
|
||||
c.req.path.startsWith("/v1/customers") && method === "POST";
|
||||
|
||||
return {
|
||||
customerId: isCreateCustomerPath ? body?.id : body?.customer_id,
|
||||
sendEvent: body?.send_event,
|
||||
};
|
||||
} catch (_error) {
|
||||
// Body might not be JSON, that's okay
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs response details asynchronously without blocking
|
||||
*/
|
||||
@@ -109,6 +137,11 @@ export const analyticsMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
||||
|
||||
ctx.logger.info(`${method} ${c.req.path} (${ctx.org?.slug})`);
|
||||
|
||||
Sentry.setUser({
|
||||
...reqContext,
|
||||
body: undefined,
|
||||
});
|
||||
|
||||
// Execute the request
|
||||
await next();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ErrCode, RecaseError as SharedRecaseError } from "@autumn/shared";
|
||||
import * as Sentry from "@sentry/bun";
|
||||
import type { Context } from "hono";
|
||||
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import Stripe from "stripe";
|
||||
@@ -7,7 +8,6 @@ import { formatZodError } from "@/errors/formatZodError.js";
|
||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { handleErrorSkip } from "./errorSkipMiddleware.js";
|
||||
|
||||
/**
|
||||
* Hono error handler middleware
|
||||
* Handles different error types and responds appropriately with proper logging
|
||||
@@ -19,6 +19,7 @@ export const errorMiddleware = (err: Error, c: Context<HonoEnv>) => {
|
||||
// If no context/logger available, fallback to console
|
||||
if (!logger) {
|
||||
console.error("Error occurred before context was set:", err);
|
||||
Sentry.captureException(err);
|
||||
return c.json(
|
||||
{
|
||||
message: "Internal server error",
|
||||
@@ -28,10 +29,13 @@ export const errorMiddleware = (err: Error, c: Context<HonoEnv>) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Check for error skip cases first (warn-level errors)
|
||||
// Check for error skip cases first (warn-level errors that don't need Sentry)
|
||||
const skipResponse = handleErrorSkip(err, c);
|
||||
if (skipResponse) return skipResponse;
|
||||
|
||||
// If we got here, it's an error worth tracking - capture to Sentry
|
||||
Sentry.captureException(err);
|
||||
|
||||
// 1. Handle RecaseError (our custom errors)
|
||||
if (err instanceof RecaseError || err instanceof SharedRecaseError) {
|
||||
logger.warn(
|
||||
@@ -75,39 +79,24 @@ export const errorMiddleware = (err: Error, c: Context<HonoEnv>) => {
|
||||
}
|
||||
|
||||
// 3. Handle Zod validation errors
|
||||
|
||||
// Note: User input validation errors are already handled by errorSkipMiddleware
|
||||
// If we get here, it's an internal Zod error (bug in our code)
|
||||
if (err instanceof ZodError) {
|
||||
const formattedError = formatZodError(err);
|
||||
|
||||
// 1. If it's validation error
|
||||
if (c.get("validated")) {
|
||||
logger.error(
|
||||
`INTERNAL ZOD ERROR (${ctx.org?.slug || "unknown"}): ${formattedError}`,
|
||||
);
|
||||
logger.error(err);
|
||||
logger.error(
|
||||
`INTERNAL ZOD ERROR (${ctx.org?.slug || "unknown"}): ${formattedError}`,
|
||||
);
|
||||
logger.error(err);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
message: formattedError,
|
||||
code: ErrCode.InvalidInputs,
|
||||
env: ctx.env,
|
||||
},
|
||||
500,
|
||||
);
|
||||
} else {
|
||||
logger.warn(
|
||||
`ZOD ERROR (${ctx.org?.slug || "unknown"}): ${formattedError}`,
|
||||
);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
message: formattedError,
|
||||
code: ErrCode.InvalidInputs,
|
||||
env: ctx.env,
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
return c.json(
|
||||
{
|
||||
message: formattedError,
|
||||
code: ErrCode.InvalidInputs,
|
||||
env: ctx.env,
|
||||
},
|
||||
500,
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Handle unknown errors
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import {
|
||||
CusErrorCode,
|
||||
ErrCode,
|
||||
ProductErrorCode,
|
||||
RecaseError as SharedRecaseError,
|
||||
} from "@autumn/shared";
|
||||
import { ErrCode, RecaseError as SharedRecaseError } from "@autumn/shared";
|
||||
import type { Context } from "hono";
|
||||
import type { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import Stripe from "stripe";
|
||||
@@ -34,13 +29,13 @@ const ROUTE_ERROR_SKIP_MAP = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
/** Global error codes that should be logged as warnings instead of errors (all routes) */
|
||||
const GLOBAL_WARN_ERROR_CODES: string[] = [
|
||||
ProductErrorCode.ProductNotFound,
|
||||
CusErrorCode.CustomerNotFound,
|
||||
ErrCode.CustomerNotFound,
|
||||
ErrCode.EntityNotFound,
|
||||
];
|
||||
// /** Global error codes that should be logged as warnings instead of errors (all routes) */
|
||||
// const GLOBAL_WARN_ERROR_CODES: string[] = [
|
||||
// ProductErrorCode.ProductNotFound,
|
||||
// CusErrorCode.CustomerNotFound,
|
||||
// ErrCode.CustomerNotFound,
|
||||
// ErrCode.EntityNotFound,
|
||||
// ];
|
||||
|
||||
/** Advanced route-specific error handling rules (for complex matching logic) */
|
||||
const ROUTE_SPECIFIC_RULES: Array<{
|
||||
@@ -135,6 +130,13 @@ const STRIPE_RULES = [
|
||||
|
||||
/** Zod-specific error handling rules */
|
||||
const ZOD_RULES = [
|
||||
{
|
||||
name: "Zod user input validation error",
|
||||
match: (err: Error, c: Context<HonoEnv>) =>
|
||||
err instanceof ZodError && !c.get("validated"),
|
||||
statusCode: 400,
|
||||
format: (err: ZodError) => formatZodError(err),
|
||||
},
|
||||
{
|
||||
name: "Zod error on /attach",
|
||||
match: (err: Error, c: Context<HonoEnv>) =>
|
||||
@@ -182,8 +184,19 @@ export const handleErrorSkip = (err: Error, c: Context<HonoEnv>) => {
|
||||
const ctx = c.get("ctx");
|
||||
const logger = ctx?.logger;
|
||||
|
||||
if (!logger) {
|
||||
return null; // Let main error handler deal with this
|
||||
if (!logger) return null; // Let main error handler deal with this
|
||||
|
||||
if (err instanceof SharedRecaseError) {
|
||||
logger.warn(
|
||||
`${err.message}, org: ${ctx.org?.slug || "unknown"}, path: ${c.req.path}`,
|
||||
);
|
||||
return createErrorResponse({
|
||||
c,
|
||||
ctx,
|
||||
message: err.message,
|
||||
code: err.code,
|
||||
statusCode: (err.statusCode || 400) as ContentfulStatusCode,
|
||||
});
|
||||
}
|
||||
|
||||
// 1. Check route-based error code skipping (simplest case)
|
||||
@@ -213,22 +226,22 @@ export const handleErrorSkip = (err: Error, c: Context<HonoEnv>) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check global warn-level error codes
|
||||
if (
|
||||
(err instanceof RecaseError || err instanceof SharedRecaseError) &&
|
||||
GLOBAL_WARN_ERROR_CODES.includes(err.code)
|
||||
) {
|
||||
logger.warn(
|
||||
`${err.message}, org: ${ctx.org?.slug || "unknown"}, path: ${c.req.path}`,
|
||||
);
|
||||
return createErrorResponse({
|
||||
c,
|
||||
ctx,
|
||||
message: err.message,
|
||||
code: err.code,
|
||||
statusCode: 404,
|
||||
});
|
||||
}
|
||||
// // 2. Check global warn-level error codes
|
||||
// if (
|
||||
// (err instanceof RecaseError || err instanceof SharedRecaseError) &&
|
||||
// GLOBAL_WARN_ERROR_CODES.includes(err.code)
|
||||
// ) {
|
||||
// logger.warn(
|
||||
// `${err.message}, org: ${ctx.org?.slug || "unknown"}, path: ${c.req.path}`,
|
||||
// );
|
||||
// return createErrorResponse({
|
||||
// c,
|
||||
// ctx,
|
||||
// message: err.message,
|
||||
// code: err.code,
|
||||
// statusCode: 404,
|
||||
// });
|
||||
// }
|
||||
|
||||
// 3. Check advanced route-specific rules
|
||||
for (const rule of ROUTE_SPECIFIC_RULES) {
|
||||
|
||||
@@ -1,92 +1,50 @@
|
||||
import type { Context } from "hono";
|
||||
import { rateLimiter } from "hono-rate-limiter";
|
||||
import type { HonoEnv } from "../honoUtils/HonoEnv.js";
|
||||
import type { Context, Next } from "hono";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
|
||||
import { upstash } from "../external/upstash/initUpstash";
|
||||
import {
|
||||
getRateLimiter,
|
||||
getRateLimitKey,
|
||||
getRateLimitType,
|
||||
} from "../external/upstash/rateLimitUtils";
|
||||
|
||||
/**
|
||||
* General rate limiter for all API routes: 50k requests/second per organization
|
||||
* Analytics middleware for Hono
|
||||
* Enriches logger context and logs responses
|
||||
*/
|
||||
export const generalRateLimiter = rateLimiter({
|
||||
windowMs: 1000, // 1 second
|
||||
limit: 100_000,
|
||||
standardHeaders: "draft-6",
|
||||
keyGenerator: (c: Context<HonoEnv>) => {
|
||||
const ctx = c.var.ctx;
|
||||
if (!ctx?.org?.id) {
|
||||
return "anonymous";
|
||||
}
|
||||
return `org:${ctx.org.id}:${ctx.env}`;
|
||||
},
|
||||
handler: (c: Context<HonoEnv>) => {
|
||||
return c.json(
|
||||
{
|
||||
message: "Too many requests. Please try again later.",
|
||||
code: "rate_limit_exceeded",
|
||||
},
|
||||
429,
|
||||
);
|
||||
},
|
||||
});
|
||||
export const rateLimitMiddleware = async (c: Context<HonoEnv>, next: Next) => {
|
||||
if (!upstash) {
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory function to create customer-based rate limiters
|
||||
* Key format: customer_id:org_id:env
|
||||
*/
|
||||
const createCustomerRateLimiter = ({ limit }: { limit: number }) => {
|
||||
return rateLimiter({
|
||||
windowMs: 1000, // 1 second
|
||||
limit,
|
||||
const ctx = c.get("ctx");
|
||||
try {
|
||||
// 1. Check method.
|
||||
const rateLimitType = getRateLimitType(c);
|
||||
|
||||
standardHeaders: "draft-6",
|
||||
keyGenerator: async (c: Context<HonoEnv>) => {
|
||||
const ctx = c.var.ctx;
|
||||
// 2. Get rate limit key
|
||||
const rateLimitKey = await getRateLimitKey({ c, rateLimitType });
|
||||
const rateLimiter = getRateLimiter({ rateLimitType });
|
||||
|
||||
// Try to get customer_id from request body
|
||||
let customerId: string | undefined;
|
||||
// 3. Check rate limit
|
||||
const result = await rateLimiter?.limit(rateLimitKey);
|
||||
|
||||
try {
|
||||
const bodyObj = await c.req.json();
|
||||
customerId = bodyObj?.customer_id;
|
||||
} catch {
|
||||
// If we can't parse the body, fall back to org-level limiting
|
||||
}
|
||||
|
||||
if (!customerId || !ctx?.org?.id || !ctx?.env) {
|
||||
// Fall back to org-level limiting if customer info not available
|
||||
return ctx?.org?.id
|
||||
? `org:${ctx.org.id}:${ctx.env || "unknown"}`
|
||||
: "anonymous";
|
||||
}
|
||||
|
||||
return `customer:${customerId}:${ctx.org.id}:${ctx.env}`;
|
||||
},
|
||||
handler: (c: Context<HonoEnv>) => {
|
||||
return c.json(
|
||||
{
|
||||
message: "Too many requests. Please try again later.",
|
||||
code: "rate_limit_exceeded",
|
||||
},
|
||||
429,
|
||||
if (!result?.success) {
|
||||
ctx.logger.warn(
|
||||
`${rateLimitType} - Rate limit exceeded: ${rateLimitKey}`,
|
||||
);
|
||||
},
|
||||
// store: new RedisStore({
|
||||
// client: new Redis({
|
||||
// url: process.env.UPSTASH_URL!,
|
||||
// token: process.env.UPSTASH_TOKEN!,
|
||||
// }),
|
||||
// }),
|
||||
});
|
||||
return c.json(
|
||||
{ error: "Rate limit exceeded" },
|
||||
StatusCodes.TOO_MANY_REQUESTS,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
ctx.logger.error(
|
||||
`Error checking rate limit, error: ${error}. Bypassing for now`,
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Execute the request
|
||||
await next();
|
||||
};
|
||||
|
||||
/**
|
||||
* Rate limiter for /track and /events endpoints: 1k requests/second per customer
|
||||
*/
|
||||
export const customerTrackRateLimiter = createCustomerRateLimiter({
|
||||
limit: 10_000,
|
||||
});
|
||||
|
||||
/**
|
||||
* Rate limiter for /check and /entitled endpoints: 100k requests/second per customer
|
||||
*/
|
||||
export const customerCheckRateLimiter = createCustomerRateLimiter({
|
||||
limit: 100_000,
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Skip OpenTelemetry instrumentation in development for faster startup
|
||||
if (process.env.NODE_ENV !== "development") {
|
||||
await import("./instrumentation.js");
|
||||
await import("./sentry.js");
|
||||
}
|
||||
|
||||
import cluster from "node:cluster";
|
||||
|
||||
@@ -3,30 +3,13 @@ import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { vercelWebhookRouter } from "./external/vercel/vercelWebhookRouter.js";
|
||||
import { handleConnectWebhook } from "./external/webhooks/connectWebhookRouter.js";
|
||||
import { analyticsMiddleware } from "./honoMiddlewares/analyticsMiddleware.js";
|
||||
import { apiVersionMiddleware } from "./honoMiddlewares/apiVersionMiddleware.js";
|
||||
import { baseMiddleware } from "./honoMiddlewares/baseMiddleware.js";
|
||||
import { errorMiddleware } from "./honoMiddlewares/errorMiddleware.js";
|
||||
import { orgConfigMiddleware } from "./honoMiddlewares/orgConfigMiddleware.js";
|
||||
import { queryMiddleware } from "./honoMiddlewares/queryMiddleware.js";
|
||||
import { refreshCacheMiddleware } from "./honoMiddlewares/refreshCacheMiddleware.js";
|
||||
import { secretKeyMiddleware } from "./honoMiddlewares/secretKeyMiddleware.js";
|
||||
import { traceMiddleware } from "./honoMiddlewares/traceMiddleware.js";
|
||||
import type { HonoEnv } from "./honoUtils/HonoEnv.js";
|
||||
import { handleHealthCheck } from "./honoUtils/handleHealthCheck.js";
|
||||
import { balancesRouter } from "./internal/balances/balancesRouter.js";
|
||||
import { billingRouter } from "./internal/billing/billingRouter.js";
|
||||
import { cusRouter } from "./internal/customers/cusRouter.js";
|
||||
import { entityRouter } from "./internal/entities/entityRouter.js";
|
||||
import { featureRouter } from "./internal/features/featureRouter.js";
|
||||
import { handleOAuthCallback } from "./internal/orgs/handlers/stripeHandlers/handleOAuthCallback.js";
|
||||
import { honoOrgRouter } from "./internal/orgs/orgRouter.js";
|
||||
import { platformBetaRouter } from "./internal/platform/platformBeta/platformBetaRouter.js";
|
||||
import {
|
||||
honoProductBetaRouter,
|
||||
honoProductRouter,
|
||||
migrationRouter,
|
||||
} from "./internal/products/productRouter.js";
|
||||
import { apiRouter } from "./routers/apiRouter.js";
|
||||
import { internalRouter } from "./routers/internalRouter.js";
|
||||
import { auth } from "./utils/auth.js";
|
||||
|
||||
@@ -102,29 +85,7 @@ export const createHonoApp = () => {
|
||||
app.route("/webhooks/vercel", vercelWebhookRouter);
|
||||
|
||||
// API Middleware
|
||||
app.use("/v1/*", secretKeyMiddleware);
|
||||
app.use("/v1/*", orgConfigMiddleware);
|
||||
app.use("/v1/*", apiVersionMiddleware);
|
||||
app.use("/v1/*", analyticsMiddleware);
|
||||
app.use("/v1/*", refreshCacheMiddleware);
|
||||
app.use("/v1/*", queryMiddleware());
|
||||
|
||||
app.route("v1", billingRouter);
|
||||
app.route("v1", balancesRouter);
|
||||
app.route("v1", migrationRouter);
|
||||
app.route("v1", entityRouter);
|
||||
app.route("v1/customers", cusRouter);
|
||||
|
||||
app.route("v1/products_beta", honoProductBetaRouter);
|
||||
app.route("v1/products", honoProductRouter);
|
||||
app.route("v1/plans", honoProductRouter);
|
||||
app.route("v1/features", featureRouter);
|
||||
|
||||
app.route("v1", balancesRouter);
|
||||
app.route("v1/platform", platformBetaRouter);
|
||||
app.route("v1/platform/beta", platformBetaRouter);
|
||||
app.route("v1/organization", honoOrgRouter);
|
||||
|
||||
app.route("/v1", apiRouter);
|
||||
app.route("", internalRouter);
|
||||
|
||||
app.onError(errorMiddleware);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import "dotenv/config";
|
||||
import { NodeSDK } from "@opentelemetry/sdk-node";
|
||||
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
||||
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
||||
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
||||
import { resourceFromAttributes } from "@opentelemetry/resources";
|
||||
import { NodeSDK } from "@opentelemetry/sdk-node";
|
||||
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
||||
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
||||
|
||||
// Initialize OTLP trace exporter with the endpoint URL and headers
|
||||
|
||||
@@ -3,14 +3,12 @@ import {
|
||||
type ApiEntityV1,
|
||||
type CheckParams,
|
||||
type CustomerLegacyData,
|
||||
ErrCode,
|
||||
type Feature,
|
||||
FeatureNotFoundError,
|
||||
InternalError,
|
||||
} from "@autumn/shared";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { getCreditSystemsFromFeature } from "@/internal/features/creditSystemUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { getOrCreateApiCustomer } from "../../../customers/cusUtils/getOrCreateApiCustomer.js";
|
||||
import { getCachedApiEntity } from "../../../entities/entityUtils/apiEntityCacheUtils/getCachedApiEntity.js";
|
||||
import type { CheckData } from "../checkTypes/CheckData.js";
|
||||
@@ -88,11 +86,7 @@ export const getCheckData = async ({
|
||||
});
|
||||
|
||||
if (!feature) {
|
||||
throw new RecaseError({
|
||||
message: `feature with id ${feature_id} not found`,
|
||||
code: ErrCode.FeatureNotFound,
|
||||
statusCode: StatusCodes.NOT_FOUND,
|
||||
});
|
||||
throw new FeatureNotFoundError({ featureId: feature_id });
|
||||
}
|
||||
|
||||
let apiEntity: ApiCustomer | ApiEntityV1 | undefined;
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
import type {
|
||||
Entity,
|
||||
Feature,
|
||||
FullCusEntWithFullCusProduct,
|
||||
} from "@autumn/shared";
|
||||
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
|
||||
import { performDeductionOnCusEnt } from "../../../trigger/updateBalanceTask.js";
|
||||
import { CusEntService } from "../../customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import { filterCusEnts } from "../../customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
|
||||
|
||||
export const deductFromAdditionalBalance = async ({
|
||||
cusEnts,
|
||||
entity,
|
||||
feature,
|
||||
toDeduct,
|
||||
ctx,
|
||||
}: {
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
entity?: Entity;
|
||||
feature: Feature;
|
||||
toDeduct: number;
|
||||
ctx: AutumnContext;
|
||||
}) => {
|
||||
const { db, features } = ctx;
|
||||
|
||||
const filteredCusEnts = filterCusEnts({
|
||||
cusEnts,
|
||||
feature,
|
||||
entity,
|
||||
features,
|
||||
}) as FullCusEntWithFullCusProduct[];
|
||||
|
||||
// console.log("Deducting from additional balance");
|
||||
for (let i = 0; i < filteredCusEnts.length; i++) {
|
||||
const cusEnt = filteredCusEnts[i];
|
||||
|
||||
const {
|
||||
newBalance,
|
||||
newEntities,
|
||||
toDeduct: newToDeduct,
|
||||
} = performDeductionOnCusEnt({
|
||||
cusEnt,
|
||||
toDeduct,
|
||||
entityId: entity?.id,
|
||||
allowNegativeBalance: false,
|
||||
field: "additional_balance",
|
||||
});
|
||||
|
||||
// Update cus ent in place
|
||||
filteredCusEnts[i] = {
|
||||
...cusEnt,
|
||||
additional_balance: newBalance,
|
||||
entities: newEntities,
|
||||
};
|
||||
|
||||
await CusEntService.update({
|
||||
db,
|
||||
id: cusEnt.id,
|
||||
updates: {
|
||||
additional_balance: newBalance,
|
||||
entities: newEntities,
|
||||
},
|
||||
});
|
||||
|
||||
toDeduct = newToDeduct;
|
||||
if (toDeduct === 0) break;
|
||||
}
|
||||
|
||||
return toDeduct;
|
||||
};
|
||||
@@ -1,109 +0,0 @@
|
||||
import type {
|
||||
Entity,
|
||||
Feature,
|
||||
FullCusEntWithFullCusProduct,
|
||||
} from "@autumn/shared";
|
||||
import { Decimal } from "decimal.js";
|
||||
import type { AutumnContext } from "../../../honoUtils/HonoEnv.js";
|
||||
import { performDeductionOnCusEnt } from "../../../trigger/updateBalanceTask.js";
|
||||
import { CusEntService } from "../../customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import { filterCusEnts } from "../../customers/cusProducts/cusEnts/cusEntUtils/findCusEntUtils.js";
|
||||
|
||||
export const deductFromAdditionalGrantedBalance = async ({
|
||||
cusEnts,
|
||||
entity,
|
||||
feature,
|
||||
toDeduct,
|
||||
ctx,
|
||||
}: {
|
||||
cusEnts: FullCusEntWithFullCusProduct[];
|
||||
entity?: Entity;
|
||||
feature: Feature;
|
||||
toDeduct: number;
|
||||
ctx: AutumnContext;
|
||||
}) => {
|
||||
const { db, features } = ctx;
|
||||
|
||||
const filteredCusEnts = filterCusEnts({
|
||||
cusEnts,
|
||||
feature,
|
||||
entity,
|
||||
features,
|
||||
}) as FullCusEntWithFullCusProduct[];
|
||||
|
||||
const printLogs = false;
|
||||
for (let i = 0; i < filteredCusEnts.length; i++) {
|
||||
const cusEnt = filteredCusEnts[i];
|
||||
|
||||
if (printLogs) {
|
||||
console.log("BEFORE DEDUCTION:", {
|
||||
additional_granted_balance: cusEnt.additional_granted_balance,
|
||||
additional_balance: cusEnt.additional_balance,
|
||||
toDeduct,
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
newBalance,
|
||||
newEntities,
|
||||
toDeduct: newToDeduct,
|
||||
} = performDeductionOnCusEnt({
|
||||
cusEnt,
|
||||
toDeduct,
|
||||
entityId: entity?.id,
|
||||
allowNegativeBalance: true,
|
||||
setZeroAdjustment: false,
|
||||
blockUsageLimit: true,
|
||||
field: "additional_granted_balance",
|
||||
});
|
||||
|
||||
const toDeduct2 = new Decimal(toDeduct).minus(newToDeduct).toNumber();
|
||||
|
||||
// Use updated cusEnt with new additional_granted_balance
|
||||
const updatedCusEnt = {
|
||||
...cusEnt,
|
||||
additional_granted_balance: newBalance,
|
||||
entities: newEntities,
|
||||
};
|
||||
|
||||
const { newBalance: newBalance2, newEntities: newEntities2 } =
|
||||
performDeductionOnCusEnt({
|
||||
cusEnt: updatedCusEnt,
|
||||
toDeduct: toDeduct2,
|
||||
entityId: entity?.id,
|
||||
allowNegativeBalance: true,
|
||||
setZeroAdjustment: false,
|
||||
blockUsageLimit: true,
|
||||
field: "additional_balance",
|
||||
});
|
||||
|
||||
// Update cus ent in place
|
||||
filteredCusEnts[i] = {
|
||||
...updatedCusEnt,
|
||||
additional_balance: newBalance2,
|
||||
entities: newEntities2,
|
||||
};
|
||||
|
||||
if (printLogs) {
|
||||
console.log("FINAL UPDATE:", {
|
||||
additional_granted_balance: newBalance,
|
||||
additional_balance: newBalance2,
|
||||
});
|
||||
}
|
||||
|
||||
await CusEntService.update({
|
||||
db,
|
||||
id: cusEnt.id,
|
||||
updates: {
|
||||
additional_balance: newBalance2,
|
||||
additional_granted_balance: newBalance,
|
||||
entities: newEntities2,
|
||||
},
|
||||
});
|
||||
|
||||
toDeduct = newToDeduct;
|
||||
if (toDeduct === 0) break;
|
||||
}
|
||||
|
||||
return toDeduct;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
-- Function: Handle additional_balance fields (deduct or add)
|
||||
-- Handles both customer-level and entity-level additional_balance
|
||||
-- Supports both positive remaining_amount (deduct) and negative remaining_amount (add)
|
||||
-- Also updates additional_granted_balance when alter_granted_balance is true
|
||||
-- Also updates adjustment when alter_granted_balance is true
|
||||
-- Returns deducted/added amount and updated additional_balance values
|
||||
DROP FUNCTION IF EXISTS deduct_from_additional_balance(jsonb);
|
||||
|
||||
@@ -9,7 +9,7 @@ CREATE FUNCTION deduct_from_additional_balance(params jsonb)
|
||||
RETURNS TABLE (
|
||||
additional_deducted numeric,
|
||||
new_additional_balance numeric,
|
||||
new_additional_granted_balance numeric,
|
||||
new_adjustment numeric,
|
||||
new_entities jsonb
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
@@ -17,7 +17,7 @@ AS $$
|
||||
DECLARE
|
||||
-- Extract parameters from JSONB
|
||||
current_additional_balance numeric := COALESCE((params->>'current_additional_balance')::numeric, 0);
|
||||
current_additional_granted_balance numeric := COALESCE((params->>'current_additional_granted_balance')::numeric, 0);
|
||||
current_adjustment numeric := COALESCE((params->>'current_adjustment')::numeric, 0);
|
||||
current_entities jsonb := COALESCE(params->'current_entities', '{}'::jsonb);
|
||||
remaining_amount numeric := (params->>'remaining_amount')::numeric;
|
||||
credit_cost numeric := (params->>'credit_cost')::numeric;
|
||||
@@ -28,27 +28,25 @@ DECLARE
|
||||
|
||||
total_additional_deducted numeric := 0;
|
||||
result_additional_balance numeric;
|
||||
result_additional_granted_balance numeric;
|
||||
result_adjustment numeric;
|
||||
result_entities jsonb;
|
||||
|
||||
-- Variables for entity deduction
|
||||
remaining numeric;
|
||||
entity_key text;
|
||||
entity_additional_balance numeric;
|
||||
entity_additional_granted_balance numeric;
|
||||
additional_deductible numeric;
|
||||
entity_additional_deducted numeric;
|
||||
new_entity_additional_balance numeric;
|
||||
new_entity_additional_granted_balance numeric;
|
||||
BEGIN
|
||||
-- Initialize return values
|
||||
result_additional_balance := current_additional_balance;
|
||||
result_additional_granted_balance := current_additional_granted_balance;
|
||||
result_adjustment := current_adjustment;
|
||||
result_entities := current_entities;
|
||||
|
||||
-- Skip if flag is set or remaining_amount is zero
|
||||
IF skip_additional_balance OR remaining_amount = 0 THEN
|
||||
RETURN QUERY SELECT 0::numeric, result_additional_balance, result_additional_granted_balance, result_entities;
|
||||
RETURN QUERY SELECT 0::numeric, result_additional_balance, result_adjustment, result_entities;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
@@ -70,9 +68,9 @@ BEGIN
|
||||
IF additional_deductible > 0 THEN
|
||||
-- Deduct from customer-level additional_balance
|
||||
result_additional_balance := result_additional_balance - additional_deductible;
|
||||
-- If alter_granted_balance is true, also deduct from additional_granted_balance
|
||||
-- If alter_granted_balance is true, also deduct from adjustment
|
||||
IF alter_granted_balance THEN
|
||||
result_additional_granted_balance := result_additional_granted_balance - additional_deductible;
|
||||
result_adjustment := result_adjustment - additional_deductible;
|
||||
END IF;
|
||||
-- Convert back to feature amount for tracking
|
||||
total_additional_deducted := additional_deductible / credit_cost;
|
||||
@@ -81,9 +79,9 @@ BEGIN
|
||||
ELSE
|
||||
-- Adding: subtract negative amount (which adds)
|
||||
result_additional_balance := result_additional_balance - additional_deductible;
|
||||
-- If alter_granted_balance is true, also add to additional_granted_balance
|
||||
-- If alter_granted_balance is true, also add to adjustment
|
||||
IF alter_granted_balance THEN
|
||||
result_additional_granted_balance := result_additional_granted_balance - additional_deductible;
|
||||
result_adjustment := result_adjustment - additional_deductible;
|
||||
END IF;
|
||||
-- Convert back to feature amount for tracking (will be negative)
|
||||
total_additional_deducted := additional_deductible / credit_cost;
|
||||
@@ -103,7 +101,6 @@ BEGIN
|
||||
-- Specific Entity Mode: Handle one entity's additional_balance
|
||||
-- ========================================================================
|
||||
entity_additional_balance := COALESCE((result_entities->target_entity_id->>'additional_balance')::numeric, 0);
|
||||
entity_additional_granted_balance := COALESCE((result_entities->target_entity_id->>'additional_granted_balance')::numeric, 0);
|
||||
|
||||
-- Convert feature amount to credit amount
|
||||
additional_deductible := remaining_amount * credit_cost;
|
||||
@@ -120,13 +117,12 @@ BEGIN
|
||||
ARRAY[target_entity_id, 'additional_balance'],
|
||||
to_jsonb(new_entity_additional_balance)
|
||||
);
|
||||
-- If alter_granted_balance is true, also deduct from entity-level additional_granted_balance
|
||||
-- If alter_granted_balance is true, update adjustment field
|
||||
IF alter_granted_balance THEN
|
||||
new_entity_additional_granted_balance := entity_additional_granted_balance - additional_deductible;
|
||||
result_entities := jsonb_set(
|
||||
result_entities,
|
||||
ARRAY[target_entity_id, 'additional_granted_balance'],
|
||||
to_jsonb(new_entity_additional_granted_balance)
|
||||
ARRAY[target_entity_id, 'adjustment'],
|
||||
to_jsonb(COALESCE((result_entities->target_entity_id->>'adjustment')::numeric, 0) - additional_deductible)
|
||||
);
|
||||
END IF;
|
||||
-- Convert back to feature amount and add to total
|
||||
@@ -142,13 +138,12 @@ BEGIN
|
||||
ARRAY[target_entity_id, 'additional_balance'],
|
||||
to_jsonb(new_entity_additional_balance)
|
||||
);
|
||||
-- If alter_granted_balance is true, also add to entity-level additional_granted_balance
|
||||
-- If alter_granted_balance is true, update adjustment field
|
||||
IF alter_granted_balance THEN
|
||||
new_entity_additional_granted_balance := entity_additional_granted_balance - additional_deductible;
|
||||
result_entities := jsonb_set(
|
||||
result_entities,
|
||||
ARRAY[target_entity_id, 'additional_granted_balance'],
|
||||
to_jsonb(new_entity_additional_granted_balance)
|
||||
ARRAY[target_entity_id, 'adjustment'],
|
||||
to_jsonb(COALESCE((result_entities->target_entity_id->>'adjustment')::numeric, 0) - additional_deductible)
|
||||
);
|
||||
END IF;
|
||||
-- Convert back to feature amount (will be negative) and add to total
|
||||
@@ -168,7 +163,6 @@ BEGIN
|
||||
EXIT WHEN remaining = 0;
|
||||
|
||||
entity_additional_balance := COALESCE((result_entities->entity_key->>'additional_balance')::numeric, 0);
|
||||
entity_additional_granted_balance := COALESCE((result_entities->entity_key->>'additional_granted_balance')::numeric, 0);
|
||||
|
||||
IF remaining > 0 THEN
|
||||
-- Deducting: limit to available balance
|
||||
@@ -182,13 +176,12 @@ BEGIN
|
||||
ARRAY[entity_key, 'additional_balance'],
|
||||
to_jsonb(new_entity_additional_balance)
|
||||
);
|
||||
-- If alter_granted_balance is true, also deduct from entity-level additional_granted_balance
|
||||
-- If alter_granted_balance is true, update adjustment field
|
||||
IF alter_granted_balance THEN
|
||||
new_entity_additional_granted_balance := entity_additional_granted_balance - additional_deductible;
|
||||
result_entities := jsonb_set(
|
||||
result_entities,
|
||||
ARRAY[entity_key, 'additional_granted_balance'],
|
||||
to_jsonb(new_entity_additional_granted_balance)
|
||||
ARRAY[entity_key, 'adjustment'],
|
||||
to_jsonb(COALESCE((result_entities->entity_key->>'adjustment')::numeric, 0) - additional_deductible)
|
||||
);
|
||||
END IF;
|
||||
-- Track deduction and reduce remaining
|
||||
@@ -204,13 +197,12 @@ BEGIN
|
||||
ARRAY[entity_key, 'additional_balance'],
|
||||
to_jsonb(new_entity_additional_balance)
|
||||
);
|
||||
-- If alter_granted_balance is true, also add to entity-level additional_granted_balance
|
||||
-- If alter_granted_balance is true, update adjustment field
|
||||
IF alter_granted_balance THEN
|
||||
new_entity_additional_granted_balance := entity_additional_granted_balance - remaining;
|
||||
result_entities := jsonb_set(
|
||||
result_entities,
|
||||
ARRAY[entity_key, 'additional_granted_balance'],
|
||||
to_jsonb(new_entity_additional_granted_balance)
|
||||
ARRAY[entity_key, 'adjustment'],
|
||||
to_jsonb(COALESCE((result_entities->entity_key->>'adjustment')::numeric, 0) - remaining)
|
||||
);
|
||||
END IF;
|
||||
-- Track addition and reduce remaining (will become 0 after first entity)
|
||||
@@ -223,7 +215,7 @@ BEGIN
|
||||
END IF;
|
||||
|
||||
-- Return results
|
||||
RETURN QUERY SELECT total_additional_deducted, result_additional_balance, result_additional_granted_balance, result_entities;
|
||||
RETURN QUERY SELECT total_additional_deducted, result_additional_balance, result_adjustment, result_entities;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
-- Helper function: Perform deduction calculation for a single entitlement
|
||||
-- This handles both entity-scoped and regular balance deductions
|
||||
-- Also updates additional_granted_balance when alter_granted_balance is true
|
||||
DROP FUNCTION IF EXISTS deduct_from_main_balance(jsonb);
|
||||
|
||||
CREATE FUNCTION deduct_from_main_balance(params jsonb)
|
||||
@@ -8,8 +7,7 @@ RETURNS TABLE (
|
||||
deducted numeric,
|
||||
new_balance numeric,
|
||||
new_entities jsonb,
|
||||
new_additional_granted_balance numeric,
|
||||
new_adjustment numeric -- LEGACY: can be removed when no longer needed
|
||||
new_adjustment numeric
|
||||
)
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
@@ -17,7 +15,7 @@ DECLARE
|
||||
-- Extract parameters from JSONB
|
||||
current_balance numeric := (params->>'current_balance')::numeric;
|
||||
current_entities jsonb := COALESCE(params->'current_entities', '{}'::jsonb);
|
||||
current_additional_granted_balance numeric := COALESCE((params->>'current_additional_granted_balance')::numeric, 0);
|
||||
current_adjustment numeric := COALESCE((params->>'current_adjustment')::numeric, 0);
|
||||
amount_to_deduct numeric := (params->>'amount_to_deduct')::numeric;
|
||||
credit_cost numeric := (params->>'credit_cost')::numeric;
|
||||
allow_negative boolean := COALESCE((params->>'allow_negative')::boolean, false);
|
||||
@@ -32,21 +30,18 @@ DECLARE
|
||||
deducted_amount numeric := 0;
|
||||
result_balance numeric;
|
||||
result_entities jsonb;
|
||||
result_additional_granted_balance numeric;
|
||||
result_adjustment numeric := 0; -- LEGACY: can be removed when no longer needed
|
||||
result_adjustment numeric;
|
||||
|
||||
-- Variables for entity deduction
|
||||
remaining numeric;
|
||||
entity_key text;
|
||||
entity_balance numeric;
|
||||
entity_additional_granted_balance numeric;
|
||||
deduct_amount numeric;
|
||||
new_balance numeric;
|
||||
new_entity_additional_granted_balance numeric;
|
||||
BEGIN
|
||||
|
||||
-- Initialize return values
|
||||
result_additional_granted_balance := current_additional_granted_balance;
|
||||
result_adjustment := current_adjustment;
|
||||
|
||||
-- ============================================================================
|
||||
-- CASE 1: ENTITY-SCOPED - ALL ENTITIES (no specific entity_id)
|
||||
@@ -62,7 +57,6 @@ BEGIN
|
||||
EXIT WHEN remaining = 0;
|
||||
|
||||
entity_balance := COALESCE((result_entities->entity_key->>'balance')::numeric, 0);
|
||||
entity_additional_granted_balance := COALESCE((result_entities->entity_key->>'additional_granted_balance')::numeric, 0);
|
||||
|
||||
-- Calculate deduction respecting allow_negative and min_balance
|
||||
-- Handle negative amounts (adding credits) differently
|
||||
@@ -87,16 +81,8 @@ BEGIN
|
||||
to_jsonb(new_balance)
|
||||
);
|
||||
|
||||
-- If alter_granted_balance is true, also deduct from entity-level additional_granted_balance
|
||||
-- If alter_granted_balance is true, update adjustment field to track the change
|
||||
IF alter_granted_balance THEN
|
||||
new_entity_additional_granted_balance := entity_additional_granted_balance - deduct_amount;
|
||||
result_entities := jsonb_set(
|
||||
result_entities,
|
||||
ARRAY[entity_key, 'additional_granted_balance'],
|
||||
to_jsonb(new_entity_additional_granted_balance)
|
||||
);
|
||||
|
||||
-- LEGACY: Update adjustment field to track the change (can be removed when no longer needed)
|
||||
result_entities := jsonb_set(
|
||||
result_entities,
|
||||
ARRAY[entity_key, 'adjustment'],
|
||||
@@ -116,7 +102,6 @@ BEGIN
|
||||
-- ============================================================================
|
||||
ELSIF has_entity_scope AND target_entity_id IS NOT NULL THEN
|
||||
entity_balance := COALESCE((current_entities->target_entity_id->>'balance')::numeric, 0);
|
||||
entity_additional_granted_balance := COALESCE((current_entities->target_entity_id->>'additional_granted_balance')::numeric, 0);
|
||||
|
||||
-- Calculate deduction respecting allow_negative and min_balance
|
||||
-- Handle negative amounts (adding credits) differently
|
||||
@@ -141,16 +126,8 @@ BEGIN
|
||||
to_jsonb(new_balance)
|
||||
);
|
||||
|
||||
-- If alter_granted_balance is true, also deduct from entity-level additional_granted_balance
|
||||
-- If alter_granted_balance is true, update adjustment field to track the change
|
||||
IF alter_granted_balance THEN
|
||||
new_entity_additional_granted_balance := entity_additional_granted_balance - deducted_amount;
|
||||
result_entities := jsonb_set(
|
||||
result_entities,
|
||||
ARRAY[target_entity_id, 'additional_granted_balance'],
|
||||
to_jsonb(new_entity_additional_granted_balance)
|
||||
);
|
||||
|
||||
-- LEGACY: Update adjustment field to track the change (can be removed when no longer needed)
|
||||
result_entities := jsonb_set(
|
||||
result_entities,
|
||||
ARRAY[target_entity_id, 'adjustment'],
|
||||
@@ -187,16 +164,14 @@ BEGIN
|
||||
result_balance := current_balance - deducted_amount;
|
||||
result_entities := current_entities; -- Entities unchanged for non-entity-scoped
|
||||
|
||||
-- If alter_granted_balance is true, also deduct from customer-level additional_granted_balance
|
||||
-- If alter_granted_balance is true, update adjustment field to track the change
|
||||
IF alter_granted_balance THEN
|
||||
result_additional_granted_balance := result_additional_granted_balance - deducted_amount;
|
||||
-- LEGACY: Update adjustment field to track the change (can be removed when no longer needed)
|
||||
result_adjustment := result_adjustment - deducted_amount;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Return results
|
||||
RETURN QUERY SELECT deducted_amount, result_balance, result_entities, result_additional_granted_balance, result_adjustment;
|
||||
RETURN QUERY SELECT deducted_amount, result_balance, result_entities, result_adjustment;
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ DECLARE
|
||||
END;
|
||||
skip_additional_balance boolean := COALESCE((params->>'skip_additional_balance')::boolean, false);
|
||||
alter_granted_balance boolean := COALESCE((params->>'alter_granted_balance')::boolean, false);
|
||||
overage_behaviour text := NULLIF(params->>'overage_behaviour', '');
|
||||
feature_id text := NULLIF(params->>'feature_id', '');
|
||||
|
||||
remaining_amount numeric;
|
||||
rollover_deducted numeric := 0;
|
||||
@@ -39,7 +41,7 @@ DECLARE
|
||||
-- Current state from DB
|
||||
current_balance numeric;
|
||||
current_additional_balance numeric;
|
||||
current_additional_granted_balance numeric;
|
||||
current_adjustment numeric;
|
||||
current_entities jsonb;
|
||||
|
||||
-- Balance update (alter_granted) variables
|
||||
@@ -49,13 +51,12 @@ DECLARE
|
||||
-- Additional balance deduction
|
||||
additional_deducted numeric;
|
||||
new_additional_balance numeric;
|
||||
new_additional_granted_balance numeric;
|
||||
|
||||
-- Results from deduction helper
|
||||
deducted numeric;
|
||||
new_balance numeric;
|
||||
new_entities jsonb;
|
||||
new_adjustment numeric; -- LEGACY: can be removed when no longer needed
|
||||
new_adjustment numeric;
|
||||
-- Tracking
|
||||
updates_json jsonb := '{}'::jsonb;
|
||||
result_json jsonb;
|
||||
@@ -127,21 +128,21 @@ BEGIN
|
||||
SELECT
|
||||
ce.balance,
|
||||
COALESCE(ce.additional_balance, 0),
|
||||
COALESCE(ce.additional_granted_balance, 0),
|
||||
COALESCE(ce.adjustment, 0),
|
||||
COALESCE(ce.entities, '{}'::jsonb)
|
||||
INTO
|
||||
current_balance,
|
||||
current_additional_balance,
|
||||
current_additional_granted_balance,
|
||||
current_adjustment,
|
||||
current_entities
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.id = ent_id;
|
||||
|
||||
-- STEP 2: Deduct from additional_balance (customer-level and entity-level)
|
||||
SELECT * INTO additional_deducted, new_additional_balance, new_additional_granted_balance, current_entities
|
||||
SELECT * INTO additional_deducted, new_additional_balance, new_adjustment, current_entities
|
||||
FROM deduct_from_additional_balance(jsonb_build_object(
|
||||
'current_additional_balance', current_additional_balance,
|
||||
'current_additional_granted_balance', current_additional_granted_balance,
|
||||
'current_adjustment', current_adjustment,
|
||||
'current_entities', current_entities,
|
||||
'remaining_amount', remaining_amount,
|
||||
'credit_cost', credit_cost,
|
||||
@@ -155,11 +156,11 @@ BEGIN
|
||||
remaining_amount := remaining_amount - additional_deducted;
|
||||
|
||||
-- STEP 3: Perform deduction from main balance (Pass 1: allow_negative = false)
|
||||
SELECT * INTO deducted, new_balance, new_entities, new_additional_granted_balance, new_adjustment
|
||||
SELECT * INTO deducted, new_balance, new_entities, new_adjustment
|
||||
FROM deduct_from_main_balance(jsonb_build_object(
|
||||
'current_balance', current_balance,
|
||||
'current_entities', current_entities,
|
||||
'current_additional_granted_balance', new_additional_granted_balance,
|
||||
'current_adjustment', new_adjustment,
|
||||
'amount_to_deduct', remaining_amount,
|
||||
'credit_cost', credit_cost,
|
||||
'allow_negative', false,
|
||||
@@ -176,9 +177,8 @@ BEGIN
|
||||
SET
|
||||
balance = new_balance,
|
||||
additional_balance = new_additional_balance,
|
||||
additional_granted_balance = new_additional_granted_balance,
|
||||
entities = new_entities,
|
||||
adjustment = COALESCE(adjustment, 0) + new_adjustment -- LEGACY: can be removed when no longer needed
|
||||
adjustment = new_adjustment
|
||||
WHERE ce.id = ent_id;
|
||||
ELSE
|
||||
-- Don't update entities for non-entity-scoped entitlements (keep NULL)
|
||||
@@ -186,8 +186,7 @@ BEGIN
|
||||
SET
|
||||
balance = new_balance,
|
||||
additional_balance = new_additional_balance,
|
||||
additional_granted_balance = new_additional_granted_balance,
|
||||
adjustment = COALESCE(adjustment, 0) + new_adjustment -- LEGACY: can be removed when no longer needed
|
||||
adjustment = new_adjustment
|
||||
WHERE ce.id = ent_id;
|
||||
END IF;
|
||||
|
||||
@@ -198,7 +197,7 @@ BEGIN
|
||||
jsonb_build_object(
|
||||
'balance', new_balance,
|
||||
'additional_balance', new_additional_balance,
|
||||
'additional_granted_balance', new_additional_granted_balance,
|
||||
'adjustment', new_adjustment,
|
||||
'entities', new_entities,
|
||||
'deducted', deducted + additional_deducted,
|
||||
'additional_deducted', additional_deducted
|
||||
@@ -233,12 +232,12 @@ BEGIN
|
||||
SELECT
|
||||
ce.balance,
|
||||
COALESCE(ce.additional_balance, 0),
|
||||
COALESCE(ce.additional_granted_balance, 0),
|
||||
COALESCE(ce.adjustment, 0),
|
||||
COALESCE(ce.entities, '{}'::jsonb)
|
||||
INTO
|
||||
current_balance,
|
||||
current_additional_balance,
|
||||
current_additional_granted_balance,
|
||||
current_adjustment,
|
||||
current_entities
|
||||
FROM customer_entitlements ce
|
||||
WHERE ce.id = ent_id;
|
||||
@@ -247,11 +246,11 @@ BEGIN
|
||||
new_additional_balance := current_additional_balance;
|
||||
|
||||
-- Perform deduction (Pass 2: allow_negative = true)
|
||||
SELECT * INTO deducted, new_balance, new_entities, new_additional_granted_balance, new_adjustment
|
||||
SELECT * INTO deducted, new_balance, new_entities, new_adjustment
|
||||
FROM deduct_from_main_balance(jsonb_build_object(
|
||||
'current_balance', current_balance,
|
||||
'current_entities', current_entities,
|
||||
'current_additional_granted_balance', current_additional_granted_balance,
|
||||
'current_adjustment', current_adjustment,
|
||||
'amount_to_deduct', remaining_amount,
|
||||
'credit_cost', credit_cost,
|
||||
'allow_negative', true,
|
||||
@@ -268,9 +267,8 @@ BEGIN
|
||||
SET
|
||||
balance = new_balance,
|
||||
additional_balance = new_additional_balance,
|
||||
additional_granted_balance = new_additional_granted_balance,
|
||||
entities = new_entities,
|
||||
adjustment = COALESCE(adjustment, 0) + new_adjustment -- LEGACY: can be removed when no longer needed
|
||||
adjustment = new_adjustment
|
||||
WHERE ce.id = ent_id;
|
||||
ELSE
|
||||
-- Don't update entities for non-entity-scoped entitlements (keep NULL)
|
||||
@@ -278,8 +276,7 @@ BEGIN
|
||||
SET
|
||||
balance = new_balance,
|
||||
additional_balance = new_additional_balance,
|
||||
additional_granted_balance = new_additional_granted_balance,
|
||||
adjustment = COALESCE(adjustment, 0) + new_adjustment -- LEGACY: can be removed when no longer needed
|
||||
adjustment = new_adjustment
|
||||
WHERE ce.id = ent_id;
|
||||
END IF;
|
||||
|
||||
@@ -293,7 +290,7 @@ BEGIN
|
||||
jsonb_build_object(
|
||||
'balance', new_balance,
|
||||
'additional_balance', new_additional_balance,
|
||||
'additional_granted_balance', new_additional_granted_balance,
|
||||
'adjustment', new_adjustment,
|
||||
'entities', new_entities,
|
||||
'deducted', (updates_json->ent_id->>'deducted')::numeric + deducted,
|
||||
'additional_deducted', COALESCE((updates_json->ent_id->>'additional_deducted')::numeric, 0)
|
||||
@@ -307,7 +304,7 @@ BEGIN
|
||||
jsonb_build_object(
|
||||
'balance', new_balance,
|
||||
'additional_balance', new_additional_balance,
|
||||
'additional_granted_balance', new_additional_granted_balance,
|
||||
'adjustment', new_adjustment,
|
||||
'entities', new_entities,
|
||||
'deducted', deducted,
|
||||
'additional_deducted', 0
|
||||
@@ -320,6 +317,14 @@ BEGIN
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
-- Check overage behaviour
|
||||
IF remaining_amount > 0 AND overage_behaviour = 'reject' THEN
|
||||
RAISE EXCEPTION 'INSUFFICIENT_BALANCE|featureId:%|value:%|remaining:%',
|
||||
feature_id,
|
||||
COALESCE(amount_to_deduct::text, '0'),
|
||||
remaining_amount;
|
||||
END IF;
|
||||
|
||||
-- Build final result
|
||||
result_json := jsonb_build_object(
|
||||
'updates', updates_json,
|
||||
|
||||
@@ -1,10 +1,39 @@
|
||||
import type { ApiBalance, TrackParams, TrackResponseV2 } from "@autumn/shared";
|
||||
import { InsufficientBalanceError } from "@autumn/shared";
|
||||
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
|
||||
import { getApiCustomerBase } from "../../../customers/cusUtils/apiCusUtils/getApiCustomerBase.js";
|
||||
import { getOrCreateCustomer } from "../../../customers/cusUtils/getOrCreateCustomer.js";
|
||||
import type { FeatureDeduction } from "./getFeatureDeductions.js";
|
||||
import { runDeductionTx } from "./runDeductionTx.js";
|
||||
|
||||
const catchInsufficientBalanceError = ({
|
||||
error,
|
||||
body,
|
||||
}: {
|
||||
error: any;
|
||||
body: TrackParams;
|
||||
}) => {
|
||||
// Check if it's an insufficient balance error from PostgreSQL
|
||||
if (error.message?.includes("INSUFFICIENT_BALANCE")) {
|
||||
// Parse the error message: INSUFFICIENT_BALANCE|featureId:{id}|value:{amount}|remaining:{remaining}
|
||||
const parts = error.message.split("|");
|
||||
const featureIdMatch = parts[1]?.match(/featureId:(.*)/);
|
||||
const valueMatch = parts[2]?.match(/value:(.*)/);
|
||||
|
||||
const featureId = featureIdMatch?.[1] || body.feature_id;
|
||||
const value = valueMatch?.[1]
|
||||
? Number.parseFloat(valueMatch[1])
|
||||
: (body.value ?? 1);
|
||||
|
||||
throw new InsufficientBalanceError({
|
||||
value,
|
||||
featureId,
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute PostgreSQL-based tracking with full transaction support
|
||||
*/
|
||||
@@ -37,23 +66,32 @@ export const executePostgresTracking = async ({
|
||||
withEntities: true,
|
||||
});
|
||||
|
||||
const { fullCus: updatedFullCus, actualDeductions } = await runDeductionTx({
|
||||
ctx,
|
||||
customerId: body.customer_id,
|
||||
entityId: body.entity_id,
|
||||
deductions: featureDeductions,
|
||||
overageBehaviour: body.overage_behavior,
|
||||
eventInfo: {
|
||||
event_name: body.feature_id || body.event_name || "",
|
||||
value: body.value ?? 1,
|
||||
properties: body.properties,
|
||||
timestamp: body.timestamp,
|
||||
idempotency_key: body.idempotency_key,
|
||||
},
|
||||
refreshCache: true,
|
||||
fullCus,
|
||||
skipAdditionalBalance: true,
|
||||
});
|
||||
let updatedFullCus;
|
||||
let actualDeductions;
|
||||
|
||||
try {
|
||||
const result = await runDeductionTx({
|
||||
ctx,
|
||||
customerId: body.customer_id,
|
||||
entityId: body.entity_id,
|
||||
deductions: featureDeductions,
|
||||
overageBehaviour: body.overage_behavior,
|
||||
eventInfo: {
|
||||
event_name: body.feature_id || body.event_name || "",
|
||||
value: body.value ?? 1,
|
||||
properties: body.properties,
|
||||
timestamp: body.timestamp,
|
||||
idempotency_key: body.idempotency_key,
|
||||
},
|
||||
refreshCache: true,
|
||||
fullCus,
|
||||
skipAdditionalBalance: true,
|
||||
});
|
||||
updatedFullCus = result.fullCus;
|
||||
actualDeductions = result.actualDeductions;
|
||||
} catch (error: any) {
|
||||
catchInsufficientBalanceError({ error, body });
|
||||
}
|
||||
|
||||
if (updatedFullCus) {
|
||||
const { apiCustomer } = await getApiCustomerBase({
|
||||
@@ -62,7 +100,7 @@ export const executePostgresTracking = async ({
|
||||
});
|
||||
|
||||
const balancesRes: Record<string, ApiBalance> = {};
|
||||
for (const featureId of Object.keys(actualDeductions)) {
|
||||
for (const featureId of Object.keys(actualDeductions ?? {})) {
|
||||
balancesRes[featureId] = apiCustomer.balances[featureId];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
cusProductsToCusPrices,
|
||||
type FullCusEntWithFullCusProduct,
|
||||
type FullCustomer,
|
||||
type PgDeductionUpdate,
|
||||
} from "@autumn/shared";
|
||||
import type { AutumnContext } from "../../../../honoUtils/HonoEnv";
|
||||
import { adjustAllowance } from "../../../../trigger/adjustAllowance";
|
||||
import { CusEntService } from "../../../customers/cusProducts/cusEnts/CusEntitlementService";
|
||||
import { getTotalNegativeBalance } from "../../../customers/cusProducts/cusEnts/cusEntUtils";
|
||||
|
||||
export const handlePaidAllocatedCusEnt = async ({
|
||||
ctx,
|
||||
cusEnt,
|
||||
fullCus,
|
||||
updates,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
cusEnt: FullCusEntWithFullCusProduct;
|
||||
fullCus: FullCustomer;
|
||||
updates: Record<string, PgDeductionUpdate>;
|
||||
}) => {
|
||||
const { db, env, org } = ctx;
|
||||
|
||||
const update = updates[cusEnt.id];
|
||||
|
||||
const cusPrices = cusProductsToCusPrices({
|
||||
cusProducts: fullCus.customer_products,
|
||||
});
|
||||
|
||||
// Calculate original negative balance
|
||||
const originalGrpBalance = getTotalNegativeBalance({
|
||||
cusEnt,
|
||||
balance: cusEnt.balance!,
|
||||
entities: cusEnt.entities!,
|
||||
});
|
||||
|
||||
// Calculate new negative balance from updates
|
||||
const newGrpBalance = getTotalNegativeBalance({
|
||||
cusEnt,
|
||||
balance: update.balance,
|
||||
entities: update.entities,
|
||||
});
|
||||
|
||||
const { newReplaceables, deletedReplaceables } = await adjustAllowance({
|
||||
db,
|
||||
env,
|
||||
org,
|
||||
cusPrices: cusPrices as any,
|
||||
customer: fullCus,
|
||||
affectedFeature: cusEnt.entitlement.feature,
|
||||
cusEnt: cusEnt as any,
|
||||
originalBalance: originalGrpBalance,
|
||||
newBalance: newGrpBalance,
|
||||
logger: ctx.logger,
|
||||
});
|
||||
|
||||
// Adjust balance based on replaceables
|
||||
let reUpdatedBalance = update.balance;
|
||||
|
||||
if (newReplaceables && newReplaceables.length > 0) {
|
||||
reUpdatedBalance = reUpdatedBalance - newReplaceables.length;
|
||||
} else if (deletedReplaceables && deletedReplaceables.length > 0) {
|
||||
reUpdatedBalance = reUpdatedBalance + deletedReplaceables.length;
|
||||
}
|
||||
|
||||
if (reUpdatedBalance !== update.balance) {
|
||||
await CusEntService.update({
|
||||
db,
|
||||
id: cusEnt.id,
|
||||
updates: {
|
||||
balance: reUpdatedBalance,
|
||||
},
|
||||
});
|
||||
|
||||
// Update the updates object with the new balance
|
||||
const cusEntId = cusEnt.id;
|
||||
updates[cusEntId].balance = reUpdatedBalance;
|
||||
updates[cusEntId].newReplaceables = newReplaceables ?? undefined;
|
||||
updates[cusEntId].deletedReplaceables = deletedReplaceables ?? undefined;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
cusProductsToCusEnts,
|
||||
type FullCustomer,
|
||||
type PgDeductionUpdate,
|
||||
} from "@autumn/shared";
|
||||
import type { AutumnContext } from "../../../../honoUtils/HonoEnv";
|
||||
import { CusEntService } from "../../../customers/cusProducts/cusEnts/CusEntitlementService";
|
||||
|
||||
export const rollbackDeduction = async ({
|
||||
ctx,
|
||||
oldFullCus,
|
||||
updates,
|
||||
}: {
|
||||
ctx: AutumnContext;
|
||||
oldFullCus: FullCustomer;
|
||||
updates: Record<string, PgDeductionUpdate>;
|
||||
}) => {
|
||||
const { db, logger } = ctx;
|
||||
|
||||
logger.warn(
|
||||
`[ROLLBACK] Starting rollback for ${Object.keys(updates).length} entitlements`,
|
||||
);
|
||||
|
||||
const cusEnts = cusProductsToCusEnts({
|
||||
cusProducts: oldFullCus.customer_products,
|
||||
});
|
||||
|
||||
// For each updated entitlement, restore to original state from oldFullCus
|
||||
for (const cusEntId of Object.keys(updates)) {
|
||||
try {
|
||||
// Find the original cusEnt from oldFullCus
|
||||
const originalCusEnt = cusEnts.find((ce) => ce.id === cusEntId);
|
||||
|
||||
if (!originalCusEnt) {
|
||||
logger.error(
|
||||
`[ROLLBACK] Could not find original cusEnt ${cusEntId} in oldFullCus`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Restore the entitlement to original values
|
||||
await CusEntService.update({
|
||||
db,
|
||||
id: cusEntId,
|
||||
updates: {
|
||||
balance: originalCusEnt.balance,
|
||||
additional_balance: originalCusEnt.additional_balance,
|
||||
adjustment: originalCusEnt.adjustment,
|
||||
entities: originalCusEnt.entities,
|
||||
},
|
||||
});
|
||||
|
||||
logger.info(
|
||||
`[ROLLBACK] Successfully restored cusEnt ${cusEntId} to original state`,
|
||||
);
|
||||
} catch (error) {
|
||||
// Best effort - log error but continue with other rollbacks
|
||||
logger.error(
|
||||
`[ROLLBACK] Failed to rollback cusEnt ${cusEntId}: ${error}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn("[ROLLBACK] Rollback completed");
|
||||
};
|
||||
@@ -7,12 +7,10 @@ import {
|
||||
CusProductStatus,
|
||||
cusEntToCusPrice,
|
||||
cusProductsToCusEnts,
|
||||
cusProductsToCusPrices,
|
||||
FeatureUsageType,
|
||||
type FullCustomer,
|
||||
getMaxOverage,
|
||||
getRelevantFeatures,
|
||||
InsufficientBalanceError,
|
||||
InternalError,
|
||||
notNullish,
|
||||
nullish,
|
||||
@@ -21,22 +19,17 @@ import {
|
||||
} from "@autumn/shared";
|
||||
import { Decimal } from "decimal.js";
|
||||
import { sql } from "drizzle-orm";
|
||||
import type { DrizzleCli } from "../../../../db/initDrizzle.js";
|
||||
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
|
||||
import { adjustAllowance } from "../../../../trigger/adjustAllowance.js";
|
||||
import { EventService } from "../../../api/events/EventService.js";
|
||||
import { CusService } from "../../../customers/CusService.js";
|
||||
import { CusEntService } from "../../../customers/cusProducts/cusEnts/CusEntitlementService.js";
|
||||
import {
|
||||
getTotalNegativeBalance,
|
||||
getUnlimitedAndUsageAllowed,
|
||||
} from "../../../customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { getUnlimitedAndUsageAllowed } from "../../../customers/cusProducts/cusEnts/cusEntUtils.js";
|
||||
import { deleteCachedApiCustomer } from "../../../customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.js";
|
||||
import { getCreditCost } from "../../../features/creditSystemUtils.js";
|
||||
import { isPaidContinuousUse } from "../../../features/featureUtils.js";
|
||||
import { deductFromCache } from "../redisTrackUtils/deductFromCache.js";
|
||||
import { constructEvent, type EventInfo } from "./eventUtils.js";
|
||||
import type { FeatureDeduction } from "./getFeatureDeductions.js";
|
||||
import { handlePaidAllocatedCusEnt } from "./handlePaidAllocatedCusEnt.js";
|
||||
import { rollbackDeduction } from "./rollbackDeduction.js";
|
||||
|
||||
export type DeductionTxParams = {
|
||||
ctx: AutumnContext;
|
||||
@@ -90,16 +83,6 @@ export const deductFromCusEnts = async ({
|
||||
|
||||
const printLogs = false;
|
||||
|
||||
// if (printLogs) {
|
||||
// console.log(
|
||||
// `Deductions: `,
|
||||
// deductions.map((d) => ({
|
||||
// feature_id: d.feature.id,
|
||||
// deduction: d.deduction,
|
||||
// })),
|
||||
// );
|
||||
// }
|
||||
|
||||
const isPaidAllocated = deductions.some((d) =>
|
||||
isPaidContinuousUse({
|
||||
feature: d.feature,
|
||||
@@ -169,7 +152,9 @@ export const deductFromCusEnts = async ({
|
||||
customer_entitlement_id: ce.id,
|
||||
credit_cost: creditCost,
|
||||
entity_feature_id: ce.entitlement.entity_feature_id,
|
||||
usage_allowed: ce.usage_allowed || isFreeAllocated,
|
||||
usage_allowed:
|
||||
ce.usage_allowed ||
|
||||
(isFreeAllocated && overageBehaviour !== "reject"),
|
||||
min_balance: notNullish(maxOverage) ? -maxOverage : undefined,
|
||||
add_to_adjustment: addToAdjustment,
|
||||
};
|
||||
@@ -202,6 +187,8 @@ export const deductFromCusEnts = async ({
|
||||
cus_ent_ids: cusEntIds.length > 0 ? cusEntIds : null,
|
||||
skip_additional_balance: skipAdditionalBalance,
|
||||
alter_granted_balance: alterGrantedBalance,
|
||||
overage_behaviour: overageBehaviour ?? "cap",
|
||||
feature_id: feature.id,
|
||||
})}::jsonb
|
||||
)`,
|
||||
);
|
||||
@@ -225,13 +212,6 @@ export const deductFromCusEnts = async ({
|
||||
|
||||
const { updates, remaining: featureRemaining } = resultJson;
|
||||
|
||||
if (featureRemaining > 0 && overageBehaviour === "reject") {
|
||||
throw new InsufficientBalanceError({
|
||||
value: toDeduct ?? 1,
|
||||
featureId: feature.id,
|
||||
});
|
||||
}
|
||||
|
||||
// Track the maximum remaining amount across all deductions
|
||||
remainingAmounts[feature.id] = featureRemaining;
|
||||
|
||||
@@ -274,73 +254,34 @@ export const deductFromCusEnts = async ({
|
||||
}
|
||||
|
||||
// Bill on Stripe for each updated entitlement
|
||||
const cusPrices = cusProductsToCusPrices({
|
||||
cusProducts: fullCus.customer_products,
|
||||
});
|
||||
|
||||
for (const cusEntId of Object.keys(updates)) {
|
||||
const update = updates[cusEntId];
|
||||
const cusEnt = cusEnts.find((ce) => ce.id === cusEntId);
|
||||
try {
|
||||
for (const cusEntId of Object.keys(updates)) {
|
||||
const update = updates[cusEntId];
|
||||
const cusEnt = cusEnts.find((ce) => ce.id === cusEntId);
|
||||
|
||||
if (!cusEnt) continue;
|
||||
if (!cusEnt) continue;
|
||||
|
||||
// Calculate original negative balance
|
||||
const originalGrpBalance = getTotalNegativeBalance({
|
||||
cusEnt,
|
||||
balance: cusEnt.balance!,
|
||||
entities: cusEnt.entities!,
|
||||
});
|
||||
|
||||
// Calculate new negative balance from updates
|
||||
const newGrpBalance = getTotalNegativeBalance({
|
||||
cusEnt,
|
||||
balance: update.balance,
|
||||
entities: update.entities,
|
||||
});
|
||||
|
||||
const { newReplaceables, deletedReplaceables } = await adjustAllowance({
|
||||
db,
|
||||
env,
|
||||
org,
|
||||
cusPrices: cusPrices as any,
|
||||
customer: fullCus,
|
||||
affectedFeature: feature,
|
||||
cusEnt: cusEnt as any,
|
||||
originalBalance: originalGrpBalance,
|
||||
newBalance: newGrpBalance,
|
||||
logger: ctx.logger,
|
||||
});
|
||||
|
||||
// Adjust balance based on replaceables
|
||||
let reUpdatedBalance = update.balance;
|
||||
|
||||
if (newReplaceables && newReplaceables.length > 0) {
|
||||
reUpdatedBalance = reUpdatedBalance - newReplaceables.length;
|
||||
} else if (deletedReplaceables && deletedReplaceables.length > 0) {
|
||||
reUpdatedBalance = reUpdatedBalance + deletedReplaceables.length;
|
||||
}
|
||||
|
||||
if (reUpdatedBalance !== update.balance) {
|
||||
await CusEntService.update({
|
||||
db,
|
||||
id: cusEntId,
|
||||
updates: {
|
||||
balance: reUpdatedBalance,
|
||||
},
|
||||
await handlePaidAllocatedCusEnt({
|
||||
ctx,
|
||||
cusEnt,
|
||||
fullCus,
|
||||
updates,
|
||||
});
|
||||
|
||||
// Update the updates object with the new balance
|
||||
updates[cusEntId].balance = reUpdatedBalance;
|
||||
updates[cusEntId].newReplaceables = newReplaceables ?? undefined;
|
||||
updates[cusEntId].deletedReplaceables =
|
||||
deletedReplaceables ?? undefined;
|
||||
updateCusEntInFullCus({
|
||||
fullCus,
|
||||
cusEntId,
|
||||
update,
|
||||
});
|
||||
}
|
||||
|
||||
updateCusEntInFullCus({
|
||||
fullCus,
|
||||
cusEntId,
|
||||
update,
|
||||
} catch (error) {
|
||||
await rollbackDeduction({
|
||||
ctx,
|
||||
oldFullCus,
|
||||
updates,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,78 +313,62 @@ export const runDeductionTx = async (
|
||||
let event: Event | undefined;
|
||||
let actualDeductions: Record<string, number> = {};
|
||||
|
||||
const result = await db.transaction(
|
||||
async (tx) => {
|
||||
// Set statement timeout to cancel query at database level if it takes too long
|
||||
// This will automatically rollback the transaction and free the connection
|
||||
await tx.execute(sql`SET LOCAL statement_timeout = '1000ms'`);
|
||||
const result = await deductFromCusEnts(params);
|
||||
fullCus = result.fullCus;
|
||||
actualDeductions = result.actualDeductions;
|
||||
|
||||
// Pass tx as the db connection
|
||||
const txParams = {
|
||||
...params,
|
||||
ctx: {
|
||||
...ctx,
|
||||
db: tx as unknown as typeof db,
|
||||
},
|
||||
};
|
||||
if (!fullCus) {
|
||||
return {
|
||||
fullCus,
|
||||
event,
|
||||
actualDeductions,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await deductFromCusEnts(txParams);
|
||||
fullCus = result.fullCus;
|
||||
actualDeductions = result.actualDeductions;
|
||||
if (params.eventInfo) {
|
||||
const newEvent = constructEvent({
|
||||
ctx,
|
||||
eventInfo: params.eventInfo,
|
||||
internalCustomerId: fullCus.internal_id,
|
||||
internalEntityId: fullCus.entity?.internal_id,
|
||||
customerId: fullCus.id ?? "",
|
||||
entityId: fullCus.entity?.id,
|
||||
});
|
||||
|
||||
if (!fullCus) return;
|
||||
|
||||
if (params.eventInfo) {
|
||||
const newEvent = constructEvent({
|
||||
ctx: txParams.ctx,
|
||||
eventInfo: params.eventInfo,
|
||||
internalCustomerId: fullCus.internal_id,
|
||||
internalEntityId: fullCus.entity?.internal_id,
|
||||
customerId: fullCus.id ?? "",
|
||||
entityId: fullCus.entity?.id,
|
||||
});
|
||||
|
||||
event = await EventService.insert({
|
||||
db: tx as unknown as DrizzleCli,
|
||||
event: newEvent,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
},
|
||||
{
|
||||
isolationLevel: "read committed",
|
||||
},
|
||||
);
|
||||
event = await EventService.insert({
|
||||
db,
|
||||
event: newEvent,
|
||||
});
|
||||
}
|
||||
|
||||
if (params?.refreshCache && fullCus) {
|
||||
// 1. If paid allocated, delete cache
|
||||
if (result?.isPaidAllocated) {
|
||||
await deleteCachedApiCustomer({
|
||||
customerId: fullCus.id ?? "",
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
});
|
||||
} else {
|
||||
for (const [featureId, deductedAmount] of Object.entries(
|
||||
actualDeductions,
|
||||
)) {
|
||||
if (deductedAmount !== 0) {
|
||||
// Only deduct if something was actually deducted
|
||||
// console.log(
|
||||
// `Deducting from Redis cache: ${featureId}, ${deductedAmount}`,
|
||||
// );
|
||||
await deductFromCache({
|
||||
ctx,
|
||||
customerId: fullCus.id ?? "",
|
||||
featureId,
|
||||
amount: deductedAmount,
|
||||
entityId: params.entityId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Log summary comparison
|
||||
}
|
||||
await deleteCachedApiCustomer({
|
||||
customerId: fullCus.id ?? "",
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
});
|
||||
// // 1. If paid allocated, delete cache
|
||||
// if (result?.isPaidAllocated) {
|
||||
// await deleteCachedApiCustomer({
|
||||
// customerId: fullCus.id ?? "",
|
||||
// orgId: ctx.org.id,
|
||||
// env: ctx.env,
|
||||
// });
|
||||
// } else {
|
||||
// for (const [featureId, deductedAmount] of Object.entries(
|
||||
// actualDeductions,
|
||||
// )) {
|
||||
// if (deductedAmount !== 0) {
|
||||
// await deductFromCache({
|
||||
// ctx,
|
||||
// customerId: fullCus.id ?? "",
|
||||
// featureId,
|
||||
// amount: deductedAmount,
|
||||
// entityId: params.entityId,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -41,11 +41,7 @@ export const initCusEntEntities = ({
|
||||
for (const entity of entities) {
|
||||
if (!entitlementLinkedToEntity({ entitlement, entity })) continue;
|
||||
|
||||
if (
|
||||
existingCusEnt &&
|
||||
existingCusEnt.entities &&
|
||||
existingCusEnt.entities[entity.id]
|
||||
) {
|
||||
if (existingCusEnt?.entities?.[entity.id]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -58,7 +54,6 @@ export const initCusEntEntities = ({
|
||||
balance: resetBalance || 0,
|
||||
adjustment: 0,
|
||||
additional_balance: 0,
|
||||
additional_granted_balance: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,15 +20,13 @@ export const getResetBalancesUpdate = ({
|
||||
for (const entityId in newEntities) {
|
||||
newEntities[entityId].balance = newBalance;
|
||||
newEntities[entityId].adjustment = 0;
|
||||
newEntities[entityId].additional_balance = 0;
|
||||
newEntities[entityId].additional_granted_balance = 0;
|
||||
}
|
||||
update = { entities: newEntities };
|
||||
} else {
|
||||
update = {
|
||||
balance: newBalance,
|
||||
additional_balance: 0,
|
||||
additional_granted_balance: 0,
|
||||
adjustment: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -167,13 +167,6 @@ export const getApiBalance = ({
|
||||
};
|
||||
}
|
||||
|
||||
const totalAdjustment = sumValues(
|
||||
cusEnts.map((cusEnt) => {
|
||||
const { adjustment } = getCusEntBalance({ cusEnt, entityId });
|
||||
return adjustment;
|
||||
}),
|
||||
);
|
||||
|
||||
const totalUnused = sumValues(
|
||||
cusEnts.map((cusEnt) => {
|
||||
const { unused } = getCusEntBalance({ cusEnt, entityId });
|
||||
@@ -197,18 +190,18 @@ export const getApiBalance = ({
|
||||
),
|
||||
);
|
||||
|
||||
const totalAdditionalGrantedBalance = sumValues(
|
||||
const totalAdjustment = sumValues(
|
||||
cusEnts.map((cusEnt) => {
|
||||
const { additional_granted_balance } = getCusEntBalance({
|
||||
const { adjustment } = getCusEntBalance({
|
||||
cusEnt,
|
||||
entityId,
|
||||
});
|
||||
return additional_granted_balance;
|
||||
return adjustment;
|
||||
}),
|
||||
);
|
||||
|
||||
const grantedBalance = new Decimal(totalGrantedBalanceWithRollovers)
|
||||
.add(totalAdditionalGrantedBalance)
|
||||
.add(totalAdjustment)
|
||||
.toNumber();
|
||||
|
||||
// 2. Purchased balance
|
||||
@@ -285,7 +278,6 @@ export const getApiBalance = ({
|
||||
data: apiBalance,
|
||||
legacyData: {
|
||||
prepaid_quantity: totalPrepaidQuantity,
|
||||
total_adjustment: totalAdjustment,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import * as Sentry from "@sentry/bun";
|
||||
|
||||
const handleResFinish = (req: any, res: any) => {
|
||||
const skipUrls = ["/v1/customers/all/search"];
|
||||
|
||||
@@ -51,6 +53,11 @@ export const analyticsMiddleware = async (req: any, res: any, next: any) => {
|
||||
user_id: req.userId || null,
|
||||
};
|
||||
|
||||
Sentry.setUser({
|
||||
...reqContext,
|
||||
body: undefined,
|
||||
});
|
||||
|
||||
if (req.span) {
|
||||
req.span.setAttributes({
|
||||
org_id: req.org?.id,
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
await import("../sentry.js");
|
||||
|
||||
import { AuthType } from "@autumn/shared";
|
||||
|
||||
import {
|
||||
DeleteMessageCommand,
|
||||
type Message,
|
||||
ReceiveMessageCommand,
|
||||
} from "@aws-sdk/client-sqs";
|
||||
import * as Sentry from "@sentry/bun";
|
||||
import type { Logger } from "pino";
|
||||
import { type DrizzleCli, initDrizzle } from "@/db/initDrizzle.js";
|
||||
import { logger } from "@/external/logtail/logtailUtils.js";
|
||||
@@ -94,6 +99,17 @@ const processMessage = async ({
|
||||
logger: workerLogger,
|
||||
});
|
||||
|
||||
if (ctx) {
|
||||
Sentry.setUser({
|
||||
org_id: ctx.org?.id,
|
||||
org_slug: ctx.org?.slug,
|
||||
env: ctx.env,
|
||||
authType: AuthType.Worker,
|
||||
jobName: job.name,
|
||||
messageId: message.MessageId,
|
||||
});
|
||||
}
|
||||
|
||||
if (actionHandlers.includes(job.name as JobName)) {
|
||||
// Note: action handlers need BullMQ queue for nested jobs
|
||||
// This will need to be refactored when migrating action handlers to SQS
|
||||
@@ -139,16 +155,14 @@ const processMessage = async ({
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
workerLogger.error(
|
||||
`Failed to process SQS job: ${job.name}, error: ${error}`,
|
||||
{
|
||||
jobName: job.name,
|
||||
error: {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
Sentry.captureException(error);
|
||||
workerLogger.error(`Failed to process SQS job: ${job.name}`, {
|
||||
jobName: job.name,
|
||||
error: {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
);
|
||||
});
|
||||
// Don't delete the message on error - it will become visible again for retry
|
||||
throw error;
|
||||
}
|
||||
|
||||
47
server/src/routers/apiRouter.ts
Normal file
47
server/src/routers/apiRouter.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { Hono } from "hono";
|
||||
import { analyticsMiddleware } from "../honoMiddlewares/analyticsMiddleware.js";
|
||||
import { apiVersionMiddleware } from "../honoMiddlewares/apiVersionMiddleware.js";
|
||||
import { orgConfigMiddleware } from "../honoMiddlewares/orgConfigMiddleware.js";
|
||||
import { queryMiddleware } from "../honoMiddlewares/queryMiddleware.js";
|
||||
import { rateLimitMiddleware } from "../honoMiddlewares/rateLimitMiddleware.js";
|
||||
import { refreshCacheMiddleware } from "../honoMiddlewares/refreshCacheMiddleware.js";
|
||||
import { secretKeyMiddleware } from "../honoMiddlewares/secretKeyMiddleware.js";
|
||||
import type { HonoEnv } from "../honoUtils/HonoEnv.js";
|
||||
import { balancesRouter } from "../internal/balances/balancesRouter.js";
|
||||
import { billingRouter } from "../internal/billing/billingRouter.js";
|
||||
import { cusRouter } from "../internal/customers/cusRouter.js";
|
||||
import { entityRouter } from "../internal/entities/entityRouter.js";
|
||||
import { featureRouter } from "../internal/features/featureRouter.js";
|
||||
import { honoOrgRouter } from "../internal/orgs/orgRouter.js";
|
||||
import { platformBetaRouter } from "../internal/platform/platformBeta/platformBetaRouter.js";
|
||||
import {
|
||||
honoProductBetaRouter,
|
||||
honoProductRouter,
|
||||
migrationRouter,
|
||||
} from "../internal/products/productRouter.js";
|
||||
|
||||
export const apiRouter = new Hono<HonoEnv>();
|
||||
|
||||
apiRouter.use("*", secretKeyMiddleware);
|
||||
apiRouter.use("*", orgConfigMiddleware);
|
||||
apiRouter.use("*", apiVersionMiddleware);
|
||||
apiRouter.use("*", analyticsMiddleware);
|
||||
apiRouter.use("*", rateLimitMiddleware);
|
||||
apiRouter.use("*", refreshCacheMiddleware);
|
||||
apiRouter.use("*", queryMiddleware());
|
||||
|
||||
apiRouter.route("", billingRouter);
|
||||
apiRouter.route("", balancesRouter);
|
||||
apiRouter.route("", migrationRouter);
|
||||
apiRouter.route("", entityRouter);
|
||||
apiRouter.route("/customers", cusRouter);
|
||||
|
||||
apiRouter.route("/products_beta", honoProductBetaRouter);
|
||||
apiRouter.route("/products", honoProductRouter);
|
||||
apiRouter.route("/plans", honoProductRouter);
|
||||
apiRouter.route("/features", featureRouter);
|
||||
|
||||
apiRouter.route("", balancesRouter);
|
||||
apiRouter.route("/platform", platformBetaRouter);
|
||||
apiRouter.route("/platform/beta", platformBetaRouter);
|
||||
apiRouter.route("/organization", honoOrgRouter);
|
||||
8
server/src/sentry.ts
Normal file
8
server/src/sentry.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as Sentry from "@sentry/bun";
|
||||
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.init({
|
||||
dsn: process.env.SENTRY_DSN,
|
||||
sendDefaultPii: true,
|
||||
});
|
||||
}
|
||||
@@ -207,7 +207,7 @@ export const performDeductionOnCusEnt = ({
|
||||
addAdjustment?: boolean;
|
||||
setZeroAdjustment?: boolean;
|
||||
blockUsageLimit?: boolean;
|
||||
field?: "balance" | "additional_balance" | "additional_granted_balance";
|
||||
field?: "balance" | "additional_balance";
|
||||
}): {
|
||||
newBalance: number;
|
||||
newEntities: Record<string, EntityBalance> | undefined;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import {
|
||||
AllowanceType,
|
||||
ApiVersionClass,
|
||||
type AppEnv,
|
||||
AuthType,
|
||||
CusProductStatus,
|
||||
type Customer,
|
||||
cusEntToIncludedUsage,
|
||||
@@ -14,7 +12,6 @@ import {
|
||||
FeatureUsageType,
|
||||
type FullCusEntWithFullCusProduct,
|
||||
type FullCustomerEntitlement,
|
||||
LATEST_VERSION,
|
||||
type Organization,
|
||||
sumValues,
|
||||
} from "@autumn/shared";
|
||||
@@ -28,8 +25,6 @@ import { deductFromApiCusRollovers } from "@/internal/customers/cusProducts/cusE
|
||||
import { getCusEntsInFeatures } from "@/internal/customers/cusUtils/cusUtils.js";
|
||||
import { featureToCreditSystem } from "@/internal/features/creditSystemUtils.js";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import { deductFromAdditionalBalance } from "../internal/balances/deductUtils/deductFromAdditionalBalance.js";
|
||||
import { generateId } from "../utils/genUtils.js";
|
||||
|
||||
import {
|
||||
deductAllowanceFromCusEnt,
|
||||
@@ -456,30 +451,6 @@ export const updateUsage = async ({
|
||||
|
||||
if (toDeduct === 0) return;
|
||||
|
||||
// 2. Deduct from additional balance
|
||||
toDeduct = await deductFromAdditionalBalance({
|
||||
toDeduct,
|
||||
cusEnts,
|
||||
feature,
|
||||
entity: customer.entity,
|
||||
ctx: {
|
||||
db,
|
||||
features,
|
||||
org,
|
||||
env,
|
||||
logger,
|
||||
id: generateId("test"),
|
||||
isPublic: false,
|
||||
authType: AuthType.SecretKey,
|
||||
apiVersion: new ApiVersionClass(LATEST_VERSION),
|
||||
timestamp: Date.now(),
|
||||
expand: [],
|
||||
skipCache: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (toDeduct === 0) return;
|
||||
|
||||
// 3. Deduct from allowance
|
||||
for (const cusEnt of cusEnts) {
|
||||
toDeduct = await deductAllowanceFromCusEnt({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ErrCode } from "@autumn/shared";
|
||||
import * as Sentry from "@sentry/bun";
|
||||
import chalk from "chalk";
|
||||
import { StatusCodes } from "http-status-codes";
|
||||
import Stripe from "stripe";
|
||||
@@ -59,6 +60,7 @@ export const handleRequestError = ({
|
||||
action: string;
|
||||
}) => {
|
||||
try {
|
||||
Sentry.captureException(error);
|
||||
const logger = req.logger;
|
||||
if (error instanceof RecaseError) {
|
||||
logger.warn(
|
||||
|
||||
@@ -10,6 +10,7 @@ console.warn = (...args: any[]) => {
|
||||
|
||||
import "dotenv/config";
|
||||
import cluster from "node:cluster";
|
||||
|
||||
import { initInfisical } from "./external/infisical/initInfisical.js";
|
||||
|
||||
// Number of worker processes (defaults to CPU cores)
|
||||
|
||||
@@ -24,11 +24,20 @@ const pro = constructProduct({
|
||||
featureId: TestFeature.Users,
|
||||
includedUsage: 5,
|
||||
}),
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Words,
|
||||
includedUsage: 300,
|
||||
}),
|
||||
constructArrearProratedItem({
|
||||
featureId: TestFeature.Workflows,
|
||||
includedUsage: 0,
|
||||
pricePerUnit: 10,
|
||||
}),
|
||||
// constructFeatureItem({
|
||||
// featureId: TestFeature.Words,
|
||||
// includedUsage: 100,
|
||||
// entityFeatureId: TestFeature.Users,
|
||||
// }) as LimitedItem,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -58,6 +67,19 @@ describe(`${chalk.yellowBright("temp: Testing add ons")}`, () => {
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
await autumn.entities.create(customerId, [
|
||||
{
|
||||
id: "1",
|
||||
name: "Entity 1",
|
||||
feature_id: TestFeature.Users,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
name: "Entity 2",
|
||||
feature_id: TestFeature.Users,
|
||||
},
|
||||
]);
|
||||
});
|
||||
return;
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { ApiVersion, ErrCode } from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
import { expectAutumnError } from "../../../utils/expectUtils/expectErrUtils";
|
||||
|
||||
const testCase = "track-allocated6";
|
||||
const customerId = testCase;
|
||||
|
||||
const pro = constructProduct({
|
||||
type: "free",
|
||||
isDefault: false,
|
||||
items: [
|
||||
constructFeatureItem({
|
||||
featureId: TestFeature.Users,
|
||||
includedUsage: 5,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
describe(`${chalk.yellowBright(`${testCase}: Tracking allocated feature with overage behavior reject`)}`, () => {
|
||||
const autumnV1: AutumnInt = new AutumnInt({ version: ApiVersion.V1_2 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
withTestClock: false,
|
||||
});
|
||||
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
});
|
||||
|
||||
// Attach product to customer
|
||||
await autumnV1.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw insufficient balance error when overage behavior is reject", async () => {
|
||||
await expectAutumnError({
|
||||
errCode: ErrCode.InsufficientBalance,
|
||||
func: async () => {
|
||||
await autumnV1.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 6,
|
||||
overage_behavior: "reject",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const curBalance = await autumnV1.customers.get(customerId);
|
||||
expect(curBalance.features[TestFeature.Users].balance).toBe(5);
|
||||
});
|
||||
test("should throw insufficient balance error when overage behavior is reject and skip_cache is true", async () => {
|
||||
await expectAutumnError({
|
||||
errCode: ErrCode.InsufficientBalance,
|
||||
func: async () => {
|
||||
await autumnV1.track(
|
||||
{
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 6,
|
||||
overage_behavior: "reject",
|
||||
},
|
||||
{
|
||||
skipCache: true,
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const curBalance = await autumnV1.customers.get(customerId);
|
||||
expect(curBalance.features[TestFeature.Users].balance).toBe(5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { beforeAll, describe, expect, test } from "bun:test";
|
||||
import { LegacyVersion, OnDecrease, OnIncrease } from "@autumn/shared";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import ctx from "@tests/utils/testInitUtils/createTestContext.js";
|
||||
import chalk from "chalk";
|
||||
import { AutumnInt } from "@/external/autumn/autumnCli.js";
|
||||
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
|
||||
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
|
||||
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
|
||||
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
|
||||
import { attachFailedPaymentMethod } from "../../../../src/external/stripe/stripeCusUtils";
|
||||
import { CusService } from "../../../../src/internal/customers/CusService";
|
||||
import { expectAutumnError } from "../../../utils/expectUtils/expectErrUtils";
|
||||
|
||||
const userItem = constructArrearProratedItem({
|
||||
featureId: TestFeature.Users,
|
||||
pricePerUnit: 50,
|
||||
includedUsage: 1,
|
||||
config: {
|
||||
on_increase: OnIncrease.BillImmediately,
|
||||
on_decrease: OnDecrease.None,
|
||||
},
|
||||
});
|
||||
|
||||
export const pro = constructProduct({
|
||||
items: [userItem],
|
||||
type: "pro",
|
||||
});
|
||||
|
||||
const testCase = "track-paid-allocated6";
|
||||
|
||||
describe(`${chalk.yellowBright(`${testCase}: Testing track usage for cont use, bill immediately but payment fails`)}`, () => {
|
||||
const customerId = testCase;
|
||||
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
|
||||
|
||||
beforeAll(async () => {
|
||||
await initProductsV0({
|
||||
ctx,
|
||||
products: [pro],
|
||||
prefix: testCase,
|
||||
customerId,
|
||||
});
|
||||
|
||||
await initCustomerV3({
|
||||
ctx,
|
||||
customerId,
|
||||
customerData: {},
|
||||
attachPm: "success",
|
||||
withTestClock: true,
|
||||
});
|
||||
|
||||
await autumn.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
});
|
||||
|
||||
const cus = await CusService.get({
|
||||
db: ctx.db,
|
||||
idOrInternalId: customerId,
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
});
|
||||
|
||||
await attachFailedPaymentMethod({
|
||||
stripeCli: ctx.stripeCli,
|
||||
customer: cus!,
|
||||
});
|
||||
});
|
||||
|
||||
test("should create track +3 usage and hit an error", async () => {
|
||||
await expectAutumnError({
|
||||
func: async () => {
|
||||
await autumn.track({
|
||||
customer_id: customerId,
|
||||
feature_id: TestFeature.Users,
|
||||
value: 3,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const customer = await autumn.customers.get(customerId);
|
||||
expect(customer.features[TestFeature.Users].balance).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,6 @@ import { z } from "zod/v4";
|
||||
|
||||
export const CusFeatureLegacyDataSchema = z.object({
|
||||
prepaid_quantity: z.number(),
|
||||
total_adjustment: z.number(),
|
||||
});
|
||||
|
||||
export type CusFeatureLegacyData = z.infer<typeof CusFeatureLegacyDataSchema>;
|
||||
|
||||
@@ -13,7 +13,6 @@ export const EntityBalanceSchema = z.object({
|
||||
adjustment: z.number(),
|
||||
|
||||
additional_balance: z.number().optional(),
|
||||
additional_granted_balance: z.number().optional(),
|
||||
});
|
||||
|
||||
export const CustomerEntitlementSchema = z.object({
|
||||
@@ -33,7 +32,6 @@ export const CustomerEntitlementSchema = z.object({
|
||||
balance: z.number().nullish().default(0),
|
||||
|
||||
additional_balance: z.number().default(0),
|
||||
additional_granted_balance: z.number().default(0),
|
||||
|
||||
usage_allowed: z.boolean().nullable(),
|
||||
next_reset_at: z.number().nullable(),
|
||||
|
||||
@@ -33,9 +33,6 @@ export const customerEntitlements = pgTable(
|
||||
|
||||
// New field, free_balance: how much balance can be deducted
|
||||
additional_balance: numeric({ mode: "number" }).notNull().default(0),
|
||||
additional_granted_balance: numeric({ mode: "number" })
|
||||
.notNull()
|
||||
.default(0),
|
||||
|
||||
// Need to work on free balance...
|
||||
entities: jsonb("entities").$type<Record<string, EntityBalance>>(),
|
||||
|
||||
@@ -12,7 +12,6 @@ export const getSummedEntityBalances = ({
|
||||
return {
|
||||
balance: 0,
|
||||
additional_balance: 0,
|
||||
additional_granted_balance: 0,
|
||||
adjustment: 0,
|
||||
unused: 0,
|
||||
count: 0,
|
||||
@@ -24,16 +23,13 @@ export const getSummedEntityBalances = ({
|
||||
(acc, curr) => acc + (curr.additional_balance ?? 0),
|
||||
0,
|
||||
),
|
||||
additional_granted_balance: Object.values(cusEnt.entities).reduce(
|
||||
(acc, curr) => acc + (curr.additional_granted_balance ?? 0),
|
||||
0,
|
||||
),
|
||||
|
||||
balance: Object.values(cusEnt.entities).reduce(
|
||||
(acc, curr) => acc + curr.balance,
|
||||
0,
|
||||
),
|
||||
adjustment: Object.values(cusEnt.entities).reduce(
|
||||
(acc, curr) => acc + curr.adjustment,
|
||||
(acc, curr) => acc + (curr.adjustment ?? 0),
|
||||
0,
|
||||
),
|
||||
unused: 0,
|
||||
@@ -50,7 +46,6 @@ export const getCusEntBalance = ({
|
||||
}): {
|
||||
balance: number;
|
||||
additional_balance: number;
|
||||
additional_granted_balance: number;
|
||||
adjustment: number;
|
||||
unused: number;
|
||||
count: number;
|
||||
@@ -70,7 +65,6 @@ export const getCusEntBalance = ({
|
||||
return {
|
||||
balance: 0,
|
||||
additional_balance: 0,
|
||||
additional_granted_balance: 0,
|
||||
adjustment: 0,
|
||||
unused: 0,
|
||||
count: 1,
|
||||
@@ -81,8 +75,6 @@ export const getCusEntBalance = ({
|
||||
balance: entityBalance || 0,
|
||||
additional_balance:
|
||||
cusEnt.entities?.[entityId]?.additional_balance || 0,
|
||||
additional_granted_balance:
|
||||
cusEnt.entities?.[entityId]?.additional_granted_balance || 0,
|
||||
adjustment,
|
||||
unused: 0,
|
||||
count: 1,
|
||||
@@ -93,7 +85,6 @@ export const getCusEntBalance = ({
|
||||
return {
|
||||
balance: cusEnt.balance || 0,
|
||||
additional_balance: cusEnt.additional_balance || 0,
|
||||
additional_granted_balance: cusEnt.additional_granted_balance || 0,
|
||||
adjustment: cusEnt.adjustment || 0,
|
||||
unused: cusEnt.replaceables?.length || 0,
|
||||
count: 1,
|
||||
|
||||
3
vite/.gitignore
vendored
3
vite/.gitignore
vendored
@@ -22,3 +22,6 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# Sentry Config File
|
||||
.env.sentry-build-plugin
|
||||
|
||||
@@ -40,6 +40,8 @@
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.1.8",
|
||||
"@sentry/react": "^10.25.0",
|
||||
"@sentry/vite-plugin": "^4.6.0",
|
||||
"@squircle/core": "^1.0.6",
|
||||
"@squircle/paint-polyfill": "^1.0.6",
|
||||
"@squircle/tailwindcss": "^1.0.6",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppEnv } from "@autumn/shared";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { init } from "@squircle/core";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
@@ -38,6 +39,12 @@ export default function App() {
|
||||
email: data.user.email,
|
||||
name: data.user.name,
|
||||
});
|
||||
Sentry.setUser({
|
||||
email: data.user.email ?? "unknown_email",
|
||||
name: data.user.name ?? "unknown_name",
|
||||
id: data.user.id ?? "unknown_user",
|
||||
orgId: data.session.activeOrganizationId ?? "unknown_org",
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
return (
|
||||
|
||||
@@ -7,12 +7,18 @@ import "./styles/form/states.css";
|
||||
|
||||
import "./styles/typography.css";
|
||||
import "./styles/custom.css";
|
||||
import * as Sentry from "@sentry/react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { PostHogProvider } from "posthog-js/react";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
Sentry.init({
|
||||
dsn: import.meta.env.VITE_SENTRY_DSN,
|
||||
sendDefaultPii: true,
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
|
||||
@@ -97,6 +97,7 @@ function UpdateCusEntitlement({
|
||||
feature_id: feature.id,
|
||||
current_balance: balanceInt,
|
||||
customer_entitlement_id: cusEnt.id,
|
||||
entity_id: entityId ?? undefined,
|
||||
// usage: 0,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import path from "node:path";
|
||||
import { sentryVitePlugin } from "@sentry/vite-plugin";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
@@ -8,9 +9,14 @@ import tsconfigPaths from "vite-tsconfig-paths";
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
tsconfigPaths(), // Automatically reads paths from tsconfig.json
|
||||
tailwindcss(), // Automatically reads paths from tsconfig.json
|
||||
tsconfigPaths(),
|
||||
sentryVitePlugin({
|
||||
org: process.env.VITE_SENTRY_ORG,
|
||||
project: process.env.VITE_SENTRY_PROJECT,
|
||||
}),
|
||||
],
|
||||
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
@@ -31,6 +37,7 @@ export default defineConfig({
|
||||
"@radix/tooltip": "@radix-ui/react-tooltip",
|
||||
},
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
// Exclude workspace dependencies from pre-bundling to avoid cache issues
|
||||
exclude: [
|
||||
@@ -43,8 +50,10 @@ export default defineConfig({
|
||||
"drizzle-orm",
|
||||
],
|
||||
},
|
||||
|
||||
// Clear cache on config change
|
||||
cacheDir: "node_modules/.vite",
|
||||
|
||||
server: {
|
||||
host: "0.0.0.0", // Required for Docker
|
||||
port: process.env.VITE_PORT
|
||||
@@ -70,4 +79,8 @@ export default defineConfig({
|
||||
allow: [".."],
|
||||
},
|
||||
},
|
||||
|
||||
build: {
|
||||
sourcemap: true,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user