Merge branch 'useautumn:staging' into staging

This commit is contained in:
Kingston
2025-08-11 21:55:38 +08:00
committed by GitHub
297 changed files with 10835 additions and 2770 deletions

3
.gitignore vendored
View File

@@ -101,4 +101,5 @@ supabase/
# To load supabase
# 1. Start supabase locally
# 2. Load files
# 2. Load files
migration.sh

150
bun.lock
View File

@@ -47,7 +47,7 @@
"@supabase/supabase-js": "^2.46.2",
"@upstash/redis": "^1.35.1",
"ai": "^4.3.10",
"autumn-js": "^0.0.77",
"autumn-js": "^0.1.4",
"axios": "^1.8.3",
"better-auth": "^1.2.9",
"body-parser": "^1.20.3",
@@ -147,7 +147,7 @@
"@clerk/clerk-react": "^5.24.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@radix-ui/react-accordion": "^1.2.3",
"@radix-ui/react-accordion": "^1.2.11",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.15",
@@ -167,7 +167,7 @@
"ag-charts-community": "^12.0.2",
"ag-grid-community": "^34.0.2",
"ag-grid-react": "^34.0.2",
"autumn-js": "^0.0.93",
"autumn-js": "^0.1.8",
"axios": "^1.8.3",
"better-auth": "^1.2.9",
"class-variance-authority": "^0.7.1",
@@ -313,15 +313,15 @@
"@browserbasehq/sdk": ["@browserbasehq/sdk@2.6.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-83iXP5D7xMm8Wyn66TUaUrgoByCmAJuoMoZQI3sGg3JAiMlTfnCIMqyVBoNSaItaPIkaCnrsj6LiusmXV2X9YA=="],
"@clerk/backend": ["@clerk/backend@2.6.2", "", { "dependencies": { "@clerk/shared": "^3.17.0", "@clerk/types": "^4.72.0", "cookie": "1.0.2", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-IUTjLmA1QkqoJnB97S8Ay/oeFR1QtBxxzi9V2J8zncGdUUpAHRp9PfbUwe203VEZuoDD8n6PGfK4oiiq5CoKhQ=="],
"@clerk/backend": ["@clerk/backend@2.6.3", "", { "dependencies": { "@clerk/shared": "^3.18.0", "@clerk/types": "^4.73.0", "cookie": "1.0.2", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-x73eu/zDMc1E2YvvW+KW55B6BOfiUrgFhSJngBsZJ6+W5hWZph+XeCS7oEcZfhvQyUcb9M6tqM2R1w7htNgh1Q=="],
"@clerk/clerk-react": ["@clerk/clerk-react@5.38.1", "", { "dependencies": { "@clerk/shared": "^3.17.0", "@clerk/types": "^4.72.0", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" } }, "sha512-IOn/Raet3jwkug8P/gLMe2nsw2wKllWGOGPFOAaaYxbXfIZ8MPngNv2/MMgVRF7cAX1UwrmU1PzrLNtBJ/EHPQ=="],
"@clerk/clerk-react": ["@clerk/clerk-react@5.39.0", "", { "dependencies": { "@clerk/shared": "^3.18.0", "@clerk/types": "^4.73.0", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" } }, "sha512-c1GJMj8wrzFrT3BbvZ4JwYY91BAY1M4yMypaAs3UNvtU0dDze7bmUVs02eW+z0GgaEQbGBVO4f5ru/pBXCGSKA=="],
"@clerk/express": ["@clerk/express@1.7.15", "", { "dependencies": { "@clerk/backend": "^2.6.2", "@clerk/shared": "^3.17.0", "@clerk/types": "^4.72.0", "tslib": "2.8.1" }, "peerDependencies": { "express": "^4.17.0 || ^5.0.0" } }, "sha512-75yh1fAl8rjPcHZKR0FT84yJLyOaAIOufHLs0SBkGVzYSX+V6pZpriiuh3QtAeXR8gSxQKF5/qMzDKpqUa/nCA=="],
"@clerk/express": ["@clerk/express@1.7.16", "", { "dependencies": { "@clerk/backend": "^2.6.3", "@clerk/shared": "^3.18.0", "@clerk/types": "^4.73.0", "tslib": "2.8.1" }, "peerDependencies": { "express": "^4.17.0 || ^5.0.0" } }, "sha512-T+Gw0vjRl9gRG1aEFfJVf3EQ9r6jfHtvOq9NU4q64GrsgvCZmvhjsx514sx3mqKfXM5CzFnT5biUpNwY27aAwQ=="],
"@clerk/shared": ["@clerk/shared@3.17.0", "", { "dependencies": { "@clerk/types": "^4.72.0", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0", "swr": "2.3.4" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-eYbA0xmKG1DluFmdVykXiElgZGTpCruEyXmIBAwokpxypd5nOpDsS1xvEKwYvZieLTZkFz21Z3Y6HdDI5cPxBQ=="],
"@clerk/shared": ["@clerk/shared@3.18.0", "", { "dependencies": { "@clerk/types": "^4.73.0", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0", "swr": "2.3.4" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-0", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-QGYbtPfJEp66AgaI0JtiRX04ejoUI8SmqUqqbAm3UXAeUAsKOyXoNbghNal97X7qeq1vPRRiDugK5B9WYN69uA=="],
"@clerk/types": ["@clerk/types@4.72.0", "", { "dependencies": { "csstype": "3.1.3" } }, "sha512-SEkgiQNeTstC0/mQjHCGBEyX0/ALyWAa5QZBBvVOok204r48MLipfIKsXQhyWE2Hk6FIo5WT6YyqD36jaxUEIw=="],
"@clerk/types": ["@clerk/types@4.73.0", "", { "dependencies": { "csstype": "3.1.3" } }, "sha512-SFM29RFVqs0KyTds3F7s7DBYNoqeBDSZiWuso/WCgfQVI4H3j0gI+0BdBEiqwCILXr4jKBG2srDmzP5FVvx8Hg=="],
"@clickhouse/client": ["@clickhouse/client@1.12.0", "", { "dependencies": { "@clickhouse/client-common": "1.12.0" } }, "sha512-vJUSX8THhTzlVn0WxPukVjOgNRaSoY02ubQkB0LpqNoHFxXuF5jQZZAYvGZWpBGbYQ/4gfPrqu8g4TX5UKeNxA=="],
@@ -571,23 +571,23 @@
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="],
"@next/env": ["@next/env@15.4.5", "", {}, "sha512-ruM+q2SCOVCepUiERoxOmZY9ZVoecR3gcXNwCYZRvQQWRjhOiPJGmQ2fAiLR6YKWXcSAh7G79KEFxN3rwhs4LQ=="],
"@next/env": ["@next/env@15.4.6", "", {}, "sha512-yHDKVTcHrZy/8TWhj0B23ylKv5ypocuCwey9ZqPyv4rPdUdRzpGCkSi03t04KBPyU96kxVtUqx6O3nE1kpxASQ=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-84dAN4fkfdC7nX6udDLz9GzQlMUwEMKD7zsseXrl7FTeIItF8vpk1lhLEnsotiiDt+QFu3O1FVWnqwcRD2U3KA=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.4.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-CL6mfGsKuFSyQjx36p2ftwMNSb8PQog8y0HO/ONLdQqDql7x3aJb/wB+LA651r4we2pp/Ck+qoRVUeZZEvSurA=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.4.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-1hTVd9n6jpM/thnDc5kYHD1OjjWYpUJrJxY4DlEacT7L5SEOXIifIdTye6SQNNn8JDZrcN+n8AWOmeJ8u3KlvQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-4W+D/nw3RpIwGrqpFi7greZ0hjrCaioGErI7XHgkcTeWdZd146NNu1s4HnaHonLeNTguKnL2Urqvj28UJj6Gqw=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.4.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-N6Mgdxe/Cn2K1yMHge6pclffkxzbSGOydXVKYOjYqQXZYjLCfN/CuFkaYDeDHY2VBwSHyM2fUjYBiQCIlxIKDA=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.5", "", { "os": "linux", "cpu": "x64" }, "sha512-YZ3bNDrS8v5KiqgWE0xZQgtXgCTUacgFtnEgI4ccotAASwSvcMPDLua7BWLuTfucoRv6mPidXkITJLd8IdJplQ=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.4.6", "", { "os": "linux", "cpu": "x64" }, "sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-9Wr4t9GkZmMNcTVvSloFtjzbH4vtT4a8+UHqDoVnxA5QyfWe6c5flTH1BIWPGNWSUlofc8dVJAE7j84FQgskvQ=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.4.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.5", "", { "os": "win32", "cpu": "x64" }, "sha512-voWk7XtGvlsP+w8VBz7lqp8Y+dYw/MTI4KeS0gTVtfdhdJ5QwhXLmNrndFOin/MDoCvUaLWMkYKATaCoUkt2/A=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.4.6", "", { "os": "win32", "cpu": "x64" }, "sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng=="],
"@noble/ciphers": ["@noble/ciphers@0.6.0", "", {}, "sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ=="],
@@ -961,13 +961,13 @@
"@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/core": ["@sentry/core@9.44.0", "", {}, "sha512-U+KBNGgq/eXIj226CPtRk+n5dx0q1xGVvbLbyfAyeek9C/wxQ3f+mvqeVqF9cx8FfrWIOeDM1F8ISH5uRkjjQg=="],
"@sentry/core": ["@sentry/core@9.44.2", "", {}, "sha512-4wduCY9vz+VRMZXTT1dzk08L2nReeR+lzpY8hCcc+Wu100BoJR+TNlrSn1rG5iIo98NDW860JsRA7SVDUDOiNQ=="],
"@sentry/node": ["@sentry/node@9.44.0", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-amqplib": "^0.46.1", "@opentelemetry/instrumentation-connect": "0.43.1", "@opentelemetry/instrumentation-dataloader": "0.16.1", "@opentelemetry/instrumentation-express": "0.47.1", "@opentelemetry/instrumentation-fs": "0.19.1", "@opentelemetry/instrumentation-generic-pool": "0.43.1", "@opentelemetry/instrumentation-graphql": "0.47.1", "@opentelemetry/instrumentation-hapi": "0.45.2", "@opentelemetry/instrumentation-http": "0.57.2", "@opentelemetry/instrumentation-ioredis": "0.47.1", "@opentelemetry/instrumentation-kafkajs": "0.7.1", "@opentelemetry/instrumentation-knex": "0.44.1", "@opentelemetry/instrumentation-koa": "0.47.1", "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", "@opentelemetry/instrumentation-mongodb": "0.52.0", "@opentelemetry/instrumentation-mongoose": "0.46.1", "@opentelemetry/instrumentation-mysql": "0.45.1", "@opentelemetry/instrumentation-mysql2": "0.45.2", "@opentelemetry/instrumentation-pg": "0.51.1", "@opentelemetry/instrumentation-redis-4": "0.46.1", "@opentelemetry/instrumentation-tedious": "0.18.1", "@opentelemetry/instrumentation-undici": "0.10.1", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.11.1", "@sentry/core": "9.44.0", "@sentry/node-core": "9.44.0", "@sentry/opentelemetry": "9.44.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" } }, "sha512-rU96Q7q7hL4s328z9zFS+ZRK6eHnLFjYbH8XHCxAxGFDLyg9kpkR5to9PjoI+QVPZ/LYAE+Xw0wStoMjWMCFsA=="],
"@sentry/node": ["@sentry/node@9.44.2", "", { "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1", "@opentelemetry/core": "^1.30.1", "@opentelemetry/instrumentation": "^0.57.2", "@opentelemetry/instrumentation-amqplib": "^0.46.1", "@opentelemetry/instrumentation-connect": "0.43.1", "@opentelemetry/instrumentation-dataloader": "0.16.1", "@opentelemetry/instrumentation-express": "0.47.1", "@opentelemetry/instrumentation-fs": "0.19.1", "@opentelemetry/instrumentation-generic-pool": "0.43.1", "@opentelemetry/instrumentation-graphql": "0.47.1", "@opentelemetry/instrumentation-hapi": "0.45.2", "@opentelemetry/instrumentation-http": "0.57.2", "@opentelemetry/instrumentation-ioredis": "0.47.1", "@opentelemetry/instrumentation-kafkajs": "0.7.1", "@opentelemetry/instrumentation-knex": "0.44.1", "@opentelemetry/instrumentation-koa": "0.47.1", "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", "@opentelemetry/instrumentation-mongodb": "0.52.0", "@opentelemetry/instrumentation-mongoose": "0.46.1", "@opentelemetry/instrumentation-mysql": "0.45.1", "@opentelemetry/instrumentation-mysql2": "0.45.2", "@opentelemetry/instrumentation-pg": "0.51.1", "@opentelemetry/instrumentation-redis-4": "0.46.1", "@opentelemetry/instrumentation-tedious": "0.18.1", "@opentelemetry/instrumentation-undici": "0.10.1", "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.11.1", "@sentry/core": "9.44.2", "@sentry/node-core": "9.44.2", "@sentry/opentelemetry": "9.44.2", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" } }, "sha512-HTUDD73Tdr4GvvcNGQunkqEKeijHb4WYq/NX4YZP5VOeOsKsgIUsv55EgWk1BSHAFGTW6bfeMSoqaNVWiRHn0w=="],
"@sentry/node-core": ["@sentry/node-core@9.44.0", "", { "dependencies": { "@sentry/core": "9.44.0", "@sentry/opentelemetry": "9.44.0", "import-in-the-middle": "^1.14.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-M6HOcA73WWzRuhqw4Fd2dqv9zEsvMteSNYOguTexIQCT2pzk1srACrt4uFfLY01s9FIKjw+tjrQfTbni2adv7Q=="],
"@sentry/node-core": ["@sentry/node-core@9.44.2", "", { "dependencies": { "@sentry/core": "9.44.2", "@sentry/opentelemetry": "9.44.2", "import-in-the-middle": "^1.14.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/instrumentation": ">=0.57.1 <1", "@opentelemetry/resources": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-TnyKZQ4FOCA+mkLLaOzFPePUBRBf0FU62hnNMscJviwb0UloOvHXx4Ub1DudfFFdnIeVSSMU96ou8vW1zR/1Uw=="],
"@sentry/opentelemetry": ["@sentry/opentelemetry@9.44.0", "", { "dependencies": { "@sentry/core": "9.44.0" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-OeMiVoLqEXtpYE2VBAGmhK4GfbUa5ivDtL+AF4B+cR+NZkqZFlnA7ItquVfAa2Jd45TIyueEK8yjan5hluQYJQ=="],
"@sentry/opentelemetry": ["@sentry/opentelemetry@9.44.2", "", { "dependencies": { "@sentry/core": "9.44.2" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" } }, "sha512-KeW5MPXyq9Q8ieYUHO0PuzNNYEYizmTH6x02PG400GwmoeNxnT59Afa4TuPcrXN0QUmK76HJuPfC+7CTuCgoKA=="],
"@sentry/types": ["@sentry/types@8.55.0", "", { "dependencies": { "@sentry/core": "8.55.0" } }, "sha512-6LRT0+r6NWQ+RtllrUW2yQfodST0cJnkOmdpHA75vONgBUhpKwiJ4H7AmgfoTET8w29pU6AnntaGOe0LJbOmog=="],
@@ -1129,7 +1129,7 @@
"@types/mysql": ["@types/mysql@2.15.27", "", { "dependencies": { "@types/node": "*" } }, "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA=="],
"@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
"@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="],
"@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="],
@@ -1181,41 +1181,41 @@
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.38.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/type-utils": "8.38.0", "@typescript-eslint/utils": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.38.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.39.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/type-utils": "8.39.0", "@typescript-eslint/utils": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.39.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.38.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.39.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.38.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.38.0", "@typescript-eslint/types": "^8.38.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.39.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.39.0", "@typescript-eslint/types": "^8.39.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0" } }, "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0" } }, "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.38.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.39.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/utils": "8.38.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.38.0", "", {}, "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.39.0", "", {}, "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.38.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.38.0", "@typescript-eslint/tsconfig-utils": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/visitor-keys": "8.38.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <5.9.0" } }, "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.39.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.39.0", "@typescript-eslint/tsconfig-utils": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/visitor-keys": "8.39.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.38.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.38.0", "@typescript-eslint/types": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.39.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.38.0", "", { "dependencies": { "@typescript-eslint/types": "8.38.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.39.0", "", { "dependencies": { "@typescript-eslint/types": "8.39.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA=="],
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20250802.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250802.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250802.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20250802.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250802.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20250802.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250802.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20250802.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-CuxxalPlgXIiAN2b884D/3UX6M1FvAi+MgruRVp4kuKOfpmBjJr6PRhN12iekFQwRG2fPil8svIm4mPnSM71lw=="],
"@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20250806.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20250806.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20250806.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20250806.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20250806.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20250806.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20250806.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20250806.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-dQWOle1t2QJHiGK+ILB5SCz1Q6VfGtkrZybT4FPSaddkskYN1djY6voxza8hd8yl1Yw0PmBmbqk3E4vTKyibig=="],
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20250802.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b8qDaguy/41W2PsA4TmGZ8wlaAg/SzG3jGJTPxv5o3tsyLQ7J/KqHljTox7RPC4tOnTzlsHYJXIuOUM+ZEIGcw=="],
"@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20250806.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rRlWJEkaNCJmd5Ph4NQAJK4NMVwhWuycMKeGxRw8Lj68kNymZTVsNi/TDYN3I1InBj7Fg2kwzygessHHzZTb8A=="],
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20250802.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-WnHvj1RcW5PeQ7vPcUhGV4F1iyl749EjiWc1FIji1nFK46AJ0NXEO6I+AeqkNTyhpnenDj1YRlqW0JyziiBZSg=="],
"@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20250806.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-n2RRV0+SHR6QnaTlJoTX6Q4M7T/P0GtWfOGiKa7QUzQFRQQNdZpTv6P2KAB38b/cfCfX7PRb6mhCWK9FMHwB3w=="],
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20250802.1", "", { "os": "linux", "cpu": "arm" }, "sha512-oEtMLt/iqADX7Dy4eVPQXzEA3kGVw47MEosHPg9CTnb9bZrEabwL8yC9knux12pqIH6zhdZAYkQIWxL6jUMOTA=="],
"@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20250806.1", "", { "os": "linux", "cpu": "arm" }, "sha512-uriaW4phv1E6YkGTacGq8YzdZ11F1lgH1kfRjwDpzNB+UDV4ONqihU0mN1y0JRHhlqqhtKhBL6UnSqd4mbu6zA=="],
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20250802.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpDnhJTsbdOihfBCbnWfF6terDfgNtW/+rwFK1VbLY6JzOQdxWHiTwP+vYA1XBmNmQTidYGNF5ahfCYwx2kftw=="],
"@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20250806.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-fwRhKF3t4OCp4ClfNrXIXwS5G+02AymnmgFGrhvnF0Wg5II8ReVVu9mvazVcfrTN9RtJo4xVofstSF2EbgQ0Sg=="],
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20250802.1", "", { "os": "linux", "cpu": "x64" }, "sha512-z+QPLdLcjkFPQ+HbBunq6U50Rd6Ywmu5ICAY4NaKgu5l1u/AyyMMhVaSINWjlZacB8CHGRl7ONb1z/vtH/iVyA=="],
"@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20250806.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EJ5/obfzrDse9KYXzZBpwzdpPad7+y/R+atXApAekIdULHLjYZ/5j/tZ9UMEnMWkCmZaFK6JpfHOG1jOGy71nA=="],
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20250802.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-diHuHgkeqKvQeP9VHMMBv7KJ0RvnPQ+ROenosRiw+sC5wButa5gR+vl1CLOtuhZXawnuRu20cxx9OFOgoODTLA=="],
"@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20250806.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-hHY/4xBsV9SWqb2nAmd9J9FQfsbte5Q4WMB2JVXSbjni0EUdDJmQgdmQ+V7v5teMaaTYhpnRrdsO95jPiR3JAA=="],
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20250802.1", "", { "os": "win32", "cpu": "x64" }, "sha512-tGTxLuIrEZG8+Pze1QWDoB6AcnoiQ6S1txaeGub0SD8nOCgAiNmqAmaJekeEYlmHm8cj8j0u2G/l+geC9StYDA=="],
"@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20250806.1", "", { "os": "win32", "cpu": "x64" }, "sha512-CFFcMz1CwZPL4TKaR10h/sZuRzGE0kLNfXq1zDDgsL1aO8gcOSDuVKrYDdwYGdedLUCINSort7kyv9nIFUKSqQ=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
@@ -1237,19 +1237,19 @@
"acorn-walk": ["acorn-walk@8.3.4", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
"ag-charts-community": ["ag-charts-community@12.1.0", "", { "dependencies": { "ag-charts-core": "12.1.0", "ag-charts-locale": "12.1.0", "ag-charts-types": "12.1.0" } }, "sha512-UEY5SAnW46sE/QNq0hTAz3VlpE2qNScP1DEqAcDzNlxO10y16/Q3E9m0uY0Jprn1jGDrkRTdAFhcSPbkqMZ+Aw=="],
"ag-charts-community": ["ag-charts-community@12.1.1", "", { "dependencies": { "ag-charts-core": "12.1.1", "ag-charts-locale": "12.1.1", "ag-charts-types": "12.1.1" } }, "sha512-5RfYXwqUr4yR1KIQAPvCsF9uaNRV6Ghs6Bj9tMn5bHL0xf1EOncEpkzXZjIXpM4yGxYk8UJKRTI3mByPjYRe8A=="],
"ag-charts-core": ["ag-charts-core@12.1.0", "", { "dependencies": { "ag-charts-types": "12.1.0" } }, "sha512-jfEqb9Ag1Bac7EoYOV8dtlGPL8AXce7+36ggL/2zfrvH/TgSflTYSh1lpGnfDkC0Q4zsl4L4StuBOK25y5/SSQ=="],
"ag-charts-core": ["ag-charts-core@12.1.1", "", { "dependencies": { "ag-charts-types": "12.1.1" } }, "sha512-Kp/fXAtNmHWWo4MTHfYN1O4O5F3wWF5ZhLcjbfrdBcx3rBdbQGw46K/FcclK8Wyer3tmITBG9EZQFO/CDeu4vA=="],
"ag-charts-locale": ["ag-charts-locale@12.1.0", "", {}, "sha512-J7hO9oYAExsN/qv7MZRB6way+q+Z3ePoTXYBvCz+GA7NQ9dgpK/dnyZbWrakF2wlAW0OqasirzhSUM93PVB8kQ=="],
"ag-charts-locale": ["ag-charts-locale@12.1.1", "", {}, "sha512-kpmtZvC12idQcNPnM3BIALmFUXvbb2TtCZtTuXqWdmtT7ykgnQCWWFczLOcM4EsgfnaudATI952JLAeQ/C2wwQ=="],
"ag-charts-react": ["ag-charts-react@12.1.0", "", { "dependencies": { "ag-charts-community": "12.1.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-V4RtA03zhUdqJNVAO/CUfCTpW2Q/Ior/cx5E6lKBtIDQkAfO0AxI/jTnukeOC3jflyiJWHe3kzGWYUhiGokjlg=="],
"ag-charts-react": ["ag-charts-react@12.1.1", "", { "dependencies": { "ag-charts-community": "12.1.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-FaFFXdICK1CVfIzCkIZN3L44ME9H6ddDAFWGcHGviA2PtfSz6WiT8fgYLIFvn7l6Qx9IcWMfqFpJ3OTf+DV8tw=="],
"ag-charts-types": ["ag-charts-types@12.1.0", "", {}, "sha512-qeODwJ1EqKjpwEbp0mQ2wQ0arRNYaZo2BafdAGfcuOwjOBlagSwJvUg5MCvAYZ/W/mg2uEmt7jKMNfDy4ul4+Q=="],
"ag-charts-types": ["ag-charts-types@12.1.1", "", {}, "sha512-VbAOfp1E7+Z/TBufJOIjUYdx70kQRDg49WoluiVKVSB0r0el/uvxARp9xjzx4ByBq9+Xq/V23tJVsRj1MS2A/g=="],
"ag-grid-community": ["ag-grid-community@34.1.0", "", { "dependencies": { "ag-charts-types": "12.1.0" } }, "sha512-3rZiOyyCGqSNqqTsrWafDVj1WfK43jfb53Ka5sqzdOG/yu6ySUFmdc0h/OuGLnkzwW5PC29coQwbS2rkb4c9dA=="],
"ag-grid-community": ["ag-grid-community@34.1.1", "", { "dependencies": { "ag-charts-types": "12.1.1" } }, "sha512-ODVvGoMTkyGvMT8b5lzvum5r93bG6CKdJdNrk6u/aYS7oqZ5rUEXJJHC8n8Zq+o76KhFiXMBQrU39xuhz8i+Tg=="],
"ag-grid-react": ["ag-grid-react@34.1.0", "", { "dependencies": { "ag-grid-community": "34.1.0", "prop-types": "^15.8.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-CY1p4/JnvcwOt2HipmsqME9CWz7M21nb3OB1DhJGOvNUaxo1wF6Hb/pKpa20F3F/E93wQCNmCz+gfrSLPuJrQw=="],
"ag-grid-react": ["ag-grid-react@34.1.1", "", { "dependencies": { "ag-grid-community": "34.1.1", "prop-types": "^15.8.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-ZffTltCJe5+yzC57LVTMFpjZiYUzFiX8+G4NNfM1MfiLV6cMl5kCEE6w3Afc0nSt8b1JCAZOiX6K0+VXqjIfYQ=="],
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
@@ -1293,7 +1293,7 @@
"attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="],
"autumn-js": ["autumn-js@0.0.77", "", { "dependencies": { "chalk": "^5.4.1", "rou3": "^0.6.1", "swr": "^2.3.3" } }, "sha512-P5btZQjaq/y4A7lcd4aK3QgZBtLZKwwQypCvJrX8SmLG/Oe+wyuWzIhdNsa/fO5ATExNmAkdP344dSUlfbgQnQ=="],
"autumn-js": ["autumn-js@0.1.8", "", { "dependencies": { "rou3": "^0.6.1", "swr": "^2.3.3", "zod": "^3.24.1" }, "peerDependencies": { "better-auth": "^1.2.12", "better-call": "^1.0.12" }, "optionalPeers": ["better-auth", "better-call"] }, "sha512-tayOSj4hFYUvfaIkmjwDyMMnKZ4duTMzuLCOXBT1HZSgO/3guAqEmaz3OQupdw/JbhpnsOEVsQfrFV2dFUU2Mw=="],
"axios": ["axios@1.11.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA=="],
@@ -1321,7 +1321,7 @@
"better-auth": ["better-auth@1.3.4", "", { "dependencies": { "@better-auth/utils": "0.2.5", "@better-fetch/fetch": "^1.1.18", "@noble/ciphers": "^0.6.0", "@noble/hashes": "^1.8.0", "@simplewebauthn/browser": "^13.0.0", "@simplewebauthn/server": "^13.0.0", "better-call": "^1.0.12", "defu": "^6.1.4", "jose": "^5.9.6", "kysely": "^0.28.1", "nanostores": "^0.11.3", "zod": "^4.0.5" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["react", "react-dom"] }, "sha512-JbZYam6Cs3Eu5CSoMK120zSshfaKvrCftSo/+v7524H1RvhryQ7UtMbzagBcXj0Digjj8hZtVkkR4tTZD/wK2g=="],
"better-call": ["better-call@1.0.12", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-ssq5OfB9Ungv2M1WVrRnMBomB0qz1VKuhkY2WxjHaLtlsHoSe9EPolj1xf7xf8LY9o3vfk3Rx6rCWI4oVHeBRg=="],
"better-call": ["better-call@1.0.13", "", { "dependencies": { "@better-fetch/fetch": "^1.1.4", "rou3": "^0.5.1", "set-cookie-parser": "^2.7.1", "uncrypto": "^0.1.3" } }, "sha512-auqdP9lnNOli9tKpZIiv0nEIwmmyaD/RotM3Mucql+Ef88etoZi/t7Ph5LjlmZt/hiSahhNTt6YVnx6++rziXA=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
@@ -1367,7 +1367,7 @@
"chai-http": ["chai-http@5.1.2", "", { "dependencies": { "@types/superagent": "^8.1.7", "charset": "^1.0.1", "cookiejar": "^2.1.4", "is-ip": "^5.0.1", "methods": "^1.1.2", "qs": "^6.12.1", "superagent": "^10.0.0" } }, "sha512-UFup7mUGkkjmi9bGA7F6vfp3lzGQZjtL//CEd+a4C+vlynSv756XHDUK8PoYk/UpTBBXqSghjQaJOUMUxJXNaA=="],
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
"chalk": ["chalk@5.5.0", "", {}, "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
@@ -1447,7 +1447,7 @@
"cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="],
"core-js": ["core-js@3.44.0", "", {}, "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw=="],
"core-js": ["core-js@3.45.0", "", {}, "sha512-c2KZL9lP4DjkN3hk/an4pWn5b5ZefhRJnAc42n6LJ19kSnbeRbdQZE5dSeE2LBol1OwJD3X1BQvFTAsa8ReeDA=="],
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
@@ -1543,7 +1543,7 @@
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"devtools-protocol": ["devtools-protocol@0.0.1464554", "", {}, "sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw=="],
"devtools-protocol": ["devtools-protocol@0.0.1475386", "", {}, "sha512-RQ809ykTfJ+dgj9bftdeL2vRVxASAuGU+I9LEx9Ij5TXU5HrgAQVmzi72VA+mkzscE12uzlRv5/tWWv9R9J1SA=="],
"dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="],
@@ -1567,7 +1567,7 @@
"drizzle-orm": ["drizzle-orm@0.43.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-dUcDaZtE/zN4RV/xqGrVSMpnEczxd5cIaoDeor7Zst9wOe/HzC/7eAaulywWGYXdDEc9oBPMjayVEDg0ziTLJA=="],
"drizzle-zod": ["drizzle-zod@0.8.2", "", { "peerDependencies": { "drizzle-orm": ">=0.36.0", "zod": "^3.25.1" } }, "sha512-9Do/16OjFFNrQDZgvMtxtDDwKWbFOxUAIwNPKX98SfxrP8H18vhN1BvNXbhelLcdgCE7GEaXDJqBjMExSkhpkA=="],
"drizzle-zod": ["drizzle-zod@0.8.3", "", { "peerDependencies": { "drizzle-orm": ">=0.36.0", "zod": "^3.25.0 || ^4.0.0" } }, "sha512-66yVOuvGhKJnTdiqj1/Xaaz9/qzOdRJADpDa68enqS6g3t0kpNkwNYjUuaeXgZfO/UWuIM9HIhSlJ6C5ZraMww=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
@@ -1575,7 +1575,7 @@
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"electron-to-chromium": ["electron-to-chromium@1.5.194", "", {}, "sha512-SdnWJwSUot04UR51I2oPD8kuP2VI37/CADR1OHsFOUzZIvfWJBO6q11k5P/uKNyTT3cdOsnyjkrZ+DDShqYqJA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.197", "", {}, "sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
@@ -1587,7 +1587,7 @@
"engine.io-parser": ["engine.io-parser@5.2.3", "", {}, "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q=="],
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
@@ -2099,7 +2099,7 @@
"netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="],
"next": ["next@15.4.5", "", { "dependencies": { "@next/env": "15.4.5", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.5", "@next/swc-darwin-x64": "15.4.5", "@next/swc-linux-arm64-gnu": "15.4.5", "@next/swc-linux-arm64-musl": "15.4.5", "@next/swc-linux-x64-gnu": "15.4.5", "@next/swc-linux-x64-musl": "15.4.5", "@next/swc-win32-arm64-msvc": "15.4.5", "@next/swc-win32-x64-msvc": "15.4.5", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-nJ4v+IO9CPmbmcvsPebIoX3Q+S7f6Fu08/dEWu0Ttfa+wVwQRh9epcmsyCPjmL2b8MxC+CkBR97jgDhUUztI3g=="],
"next": ["next@15.4.6", "", { "dependencies": { "@next/env": "15.4.6", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.4.6", "@next/swc-darwin-x64": "15.4.6", "@next/swc-linux-arm64-gnu": "15.4.6", "@next/swc-linux-arm64-musl": "15.4.6", "@next/swc-linux-x64-gnu": "15.4.6", "@next/swc-linux-x64-musl": "15.4.6", "@next/swc-win32-arm64-msvc": "15.4.6", "@next/swc-win32-x64-msvc": "15.4.6", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
@@ -2217,7 +2217,7 @@
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
"posthog-js": ["posthog-js@1.258.5", "", { "dependencies": { "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" }, "peerDependencies": { "@rrweb/types": "2.0.0-alpha.17", "rrweb-snapshot": "2.0.0-alpha.17" }, "optionalPeers": ["@rrweb/types", "rrweb-snapshot"] }, "sha512-Tx6CzS8MsGAQGPrQth5TbkGxGQgAY01SktNW773/KDmVOWiRVZq/WQF/MRJRiuFxJ7qjethZQi3aBWfWKdr1RA=="],
"posthog-js": ["posthog-js@1.258.6", "", { "dependencies": { "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" }, "peerDependencies": { "@rrweb/types": "2.0.0-alpha.17", "rrweb-snapshot": "2.0.0-alpha.17" }, "optionalPeers": ["@rrweb/types", "rrweb-snapshot"] }, "sha512-vL5AGG+rOoRg3LGquMfBPO55jD4bGl0CiV44SHdHAoBnOVDDAqxczRGDqMdxor+VLx3/ofTFOJ2FNprfAHp70Q=="],
"posthog-node": ["posthog-node@4.18.0", "", { "dependencies": { "axios": "^1.8.2" } }, "sha512-XROs1h+DNatgKh/AlIlCtDxWzwrKdYDb2mOs58n4yN8BkGN9ewqeQwG5ApS4/IzwCb7HPttUkOVulkYatd2PIw=="],
@@ -2255,7 +2255,7 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"puppeteer-core": ["puppeteer-core@24.15.0", "", { "dependencies": { "@puppeteer/browsers": "2.10.6", "chromium-bidi": "7.2.0", "debug": "^4.4.1", "devtools-protocol": "0.0.1464554", "typed-query-selector": "^2.12.0", "ws": "^8.18.3" } }, "sha512-2iy0iBeWbNyhgiCGd/wvGrDSo73emNFjSxYOcyAqYiagkYt5q4cPfVXaVDKBsukgc2fIIfLAalBZlaxldxdDYg=="],
"puppeteer-core": ["puppeteer-core@24.16.0", "", { "dependencies": { "@puppeteer/browsers": "2.10.6", "chromium-bidi": "7.2.0", "debug": "^4.4.1", "devtools-protocol": "0.0.1475386", "typed-query-selector": "^2.12.0", "ws": "^8.18.3" } }, "sha512-tZ0tJiOYaDGTRzzr2giDpf8O/55JsoqkrafS1Xu4H6S8oP4eeL6RbZzY9OzjShSf5EQvx/zAc55QKpDqzXos/Q=="],
"pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="],
@@ -2323,7 +2323,7 @@
"recaseai": ["recaseai@0.0.37", "", { "dependencies": { "@anthropic-ai/sdk": "^0.32.1", "@supabase/supabase-js": "^2.47.2", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", "@types/node": "^22.10.1", "async-listen": "^3.0.1", "axios": "^1.7.9", "commander": "^12.1.0", "cors": "^2.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", "figures": "^6.1.0", "inquirer": "^12.1.0", "ksuid": "^3.0.0", "nanoid": "^5.0.9", "openai": "^4.76.0", "ora": "^8.1.1", "picocolors": "^1.1.1", "pino": "^9.5.0", "tsx": "^4.19.2", "typescript": "^5.7.2" }, "bin": { "recase": "dist/cli.js" } }, "sha512-cKVMWTGBnGtm8K+uD2vfMXOzxdHj1U3++vvTePpOoZABI/SY/jjDpW997vM5xeDnzz7Vk/qqVkLQUQ/ZMDXpsQ=="],
"recharts": ["recharts@3.1.0", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-NqAqQcGBmLrfDs2mHX/bz8jJCQtG2FeXfE0GqpZmIuXIjkpIwj8sd9ad0WyvKiBKPd8ZgNG0hL85c8sFDwascw=="],
"recharts": ["recharts@3.1.2", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-vhNbYwaxNbk/IATK0Ki29k3qvTkGqwvCgyQAQ9MavvvBwjvKnMTswdbklJpcOAoMPN/qxF3Lyqob0zO+ZXkZ4g=="],
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
@@ -2341,7 +2341,7 @@
"reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
"resend": ["resend@4.7.0", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-30IbXGBUbmDweQH2IlO53XOXX7ndjYV9xFZ8IEBiWqefqQ/qmTsgrX0Ab6MUnmobJXbpdReVv+iXGRQPubQL5Q=="],
"resend": ["resend@4.8.0", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA=="],
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
@@ -2431,7 +2431,7 @@
"sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="],
"sonner": ["sonner@2.0.6", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-yHFhk8T/DK3YxjFQXIrcHT1rGEeTLliVzWbO0xN8GberVun2RiBnxAjXAYpZrqwEVHBG9asI/Li8TAAhN9m59Q=="],
"sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@@ -2487,7 +2487,7 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"svix": ["svix@1.70.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "@types/node": "^22.7.5", "es6-promise": "^4.2.8", "fast-sha256": "^1.3.0", "svix-fetch": "^3.0.0", "url-parse": "^1.5.10", "uuid": "^10.0.0" } }, "sha512-ZrYvUoHPPtzp+C3t5c2hvMMleeZL7teFMigt6u/gdlpEKyPopeX74eMJuHtuPixB+nB24gfWJ1W1N3B5ZZ+AJw=="],
"svix": ["svix@1.70.1", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "@types/node": "^22.7.5", "es6-promise": "^4.2.8", "fast-sha256": "^1.3.0", "svix-fetch": "^3.0.0", "url-parse": "^1.5.10", "uuid": "^10.0.0" } }, "sha512-UDO8VBNyfZcqRjQQLanrOanGK+nCaMhBT7LJyXQBhPLEUcoDzRUhKujFdivl2OrleUVNknmuuz7CnYZ+4Axugg=="],
"svix-fetch": ["svix-fetch@3.0.0", "", { "dependencies": { "node-fetch": "^2.6.1", "whatwg-fetch": "^3.4.1" } }, "sha512-rcADxEFhSqHbraZIsjyZNh4TF6V+koloX1OzZ+AQuObX9mZ2LIMhm1buZeuc5BIZPftZpJCMBsSiBaeszo9tRw=="],
@@ -2567,13 +2567,13 @@
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"typescript-eslint": ["typescript-eslint@8.38.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.38.0", "@typescript-eslint/parser": "8.38.0", "@typescript-eslint/typescript-estree": "8.38.0", "@typescript-eslint/utils": "8.38.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg=="],
"typescript-eslint": ["typescript-eslint@8.39.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.39.0", "@typescript-eslint/parser": "8.39.0", "@typescript-eslint/typescript-estree": "8.39.0", "@typescript-eslint/utils": "8.39.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q=="],
"uncrypto": ["uncrypto@0.1.3", "", {}, "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q=="],
"undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="],
@@ -2693,8 +2693,6 @@
"@autumn/vite/@types/node": ["@types/node@22.17.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ=="],
"@autumn/vite/autumn-js": ["autumn-js@0.0.93", "", { "dependencies": { "chalk": "^5.4.1", "rou3": "^0.6.1", "swr": "^2.3.3", "zod": "^3.24.1" }, "peerDependencies": { "better-auth": "^1.2.12", "better-call": "^1.0.12" }, "optionalPeers": ["better-auth", "better-call"] }, "sha512-zGq9cfkhHL6xAHlervcZHO2WQ28mHNoxKKI4vnkvKifW7ETDcQk0T7xdJS9g/a+BJh7CLqe697gEdMblMUDfUA=="],
"@autumn/vite/date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="],
"@autumn/vite/stripe": ["stripe@18.4.0", "", { "dependencies": { "qs": "^6.11.0" }, "peerDependencies": { "@types/node": ">=12.x.x" }, "optionalPeers": ["@types/node"] }, "sha512-LKFeDnDYo4U/YzNgx2Lc9PT9XgKN0JNF1iQwZxgkS4lOw5NunWCnzyH5RhTlD3clIZnf54h7nyMWkS8VXPmtTQ=="],
@@ -2713,7 +2711,7 @@
"@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@better-auth/stripe/zod": ["zod@4.0.14", "", {}, "sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw=="],
"@better-auth/stripe/zod": ["zod@4.0.15", "", {}, "sha512-2IVHb9h4Mt6+UXkyMs0XbfICUh1eUrlJJAOupBHUhLRnKkruawyDddYRCs0Eizt900ntIMk9/4RksYl+FgSpcQ=="],
"@browserbasehq/sdk/@types/node": ["@types/node@18.19.121", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ=="],
@@ -3039,25 +3037,11 @@
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/eslint-plugin/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"@typescript-eslint/parser/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"@typescript-eslint/project-service/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"@typescript-eslint/tsconfig-utils/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"@typescript-eslint/type-utils/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"@typescript-eslint/typescript-estree/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"@typescript-eslint/utils/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"better-auth/zod": ["zod@4.0.14", "", {}, "sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw=="],
"better-auth/zod": ["zod@4.0.15", "", {}, "sha512-2IVHb9h4Mt6+UXkyMs0XbfICUh1eUrlJJAOupBHUhLRnKkruawyDddYRCs0Eizt900ntIMk9/4RksYl+FgSpcQ=="],
"better-call/rou3": ["rou3@0.5.1", "", {}, "sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ=="],
@@ -3179,8 +3163,6 @@
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"typescript-eslint/typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
"winston-transport/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],

View File

@@ -48,7 +48,7 @@
"@supabase/supabase-js": "^2.46.2",
"@upstash/redis": "^1.35.1",
"ai": "^4.3.10",
"autumn-js": "^0.0.77",
"autumn-js": "^0.1.8",
"axios": "^1.8.3",
"better-auth": "^1.2.9",
"body-parser": "^1.20.3",

View File

@@ -8,7 +8,7 @@ filename=$1
if [[ $filename == *"shell"* ]]; then
$filename
$filename "${@:2}"
elif [[ $filename == *"/tests/"* ]]; then
# Extract everything after "/tests/"
path_after_tests=$(echo "$filename" | sed 's/.*\/tests\///')

View File

@@ -3,7 +3,10 @@
# Source shared configuration
source "$(dirname "$0")/config.sh"
# MOCHA_PARALLEL=true $MOCHA_SETUP \
# If contains setup then run $MOCHA_SETUP
if [[ "$2" == *"setup"* ]]; then
MOCHA_PARALLEL=true $MOCHA_SETUP
fi
$MOCHA_CMD \
'tests/attach/basic/*.ts' \

View File

@@ -15,4 +15,17 @@ $MOCHA_CMD 'tests/contUse/update/*.ts'
$MOCHA_CMD 'tests/contUse/track/*.ts'
$MOCHA_CMD 'tests/contUse/roles/*.ts'
$MOCHA_CMD 'tests/contUse/roles/*.ts'
# G4
$MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \
'tests/advanced/coupons/*.ts' \
'tests/attach/updateQuantity/*.ts' \
'tests/advanced/referrals/*.ts' \
'tests/advanced/rollovers/*.ts' \
'tests/advanced/customInterval/*.ts'
$MOCHA_CMD 'tests/attach/multiProduct/*.ts' \
'tests/advanced/usageLimit/*.ts'
$MOCHA_CMD 'tests/advanced/usage/*.ts'

View File

@@ -12,10 +12,11 @@ $MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \
'tests/advanced/coupons/*.ts' \
'tests/attach/updateQuantity/*.ts' \
'tests/advanced/referrals/*.ts' \
'tests/advanced/rollovers/*.ts'
'tests/advanced/rollovers/*.ts' \
'tests/advanced/customInterval/*.ts'
# $MOCHA_CMD 'tests/attach/multiProduct/*.ts' \
# 'tests/advanced/usageLimit/*.ts'
$MOCHA_CMD 'tests/attach/multiProduct/*.ts' \
'tests/advanced/usageLimit/*.ts'
# $MOCHA_CMD 'tests/advanced/usage/*.ts'
$MOCHA_CMD 'tests/advanced/usage/*.ts'

View File

@@ -33,13 +33,14 @@ import { createSupabaseClient } from "@/external/supabaseUtils.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import { getRelatedCusPrice } from "./internal/customers/cusProducts/cusEnts/cusEntUtils.js";
import { CusProductService } from "./internal/customers/cusProducts/CusProductService.js";
import { logSubItems } from "./utils/scriptUtils/logUtils/logSubItems.js";
const { db, client } = initDrizzle({ maxConnections: 5 });
let orgSlugs = process.env.ORG_SLUGS!.split(",");
const skipEmails = process.env.SKIP_EMAILS!.split(",");
orgSlugs = ["athenahq"];
orgSlugs = [];
const getSingleCustomer = async ({
stripeCli,
customerId,
@@ -136,7 +137,7 @@ const checkCustomerCorrect = async ({
"number of stripe subs should be the same as number of subscription ids"
);
const subItems = stripeSubs.flatMap((sub: any) => sub.items.data);
let subItems = stripeSubs.flatMap((sub: any) => sub.items.data);
const prices = cusProductToPrices({ cusProduct });
@@ -174,7 +175,7 @@ const checkCustomerCorrect = async ({
let expectedQuantity = options?.upcoming_quantity || options?.quantity;
console.log("Sub item: ", subItem);
// console.log("Sub item: ", subItem);
assert(
subItem?.quantity == expectedQuantity,
`sub item quantity for prepaid price (featureId: ${featureId}) should be ${expectedQuantity}`
@@ -282,7 +283,7 @@ export const check = async () => {
let customerId;
// customerId = "SwZ6cDgOFAY9S29nHigvPaUk4uq2";
// customerId = "94fd7303-c8ae-4873-a6cd-0f5241aef232";
let customers: FullCustomer[] = [];
let stripeSubs: Stripe.Subscription[] = [];

View File

@@ -104,10 +104,11 @@ const handleShortDurationCusEnt = async ({
let resetCusEnt = {
...cusEnt,
next_reset_at: getNextResetAt(
new UTCDate(cusEnt.next_reset_at!),
ent.interval as EntInterval
),
next_reset_at: getNextResetAt({
curReset: new UTCDate(cusEnt.next_reset_at!),
interval: ent.interval as EntInterval,
intervalCount: ent.interval_count,
}),
adjustment: 0,
...getResetBalancesUpdate({
cusEnt,
@@ -116,9 +117,6 @@ const handleShortDurationCusEnt = async ({
};
let newCusEnt = resetCusEnt;
// let { customer, customer_product, entitlement, ...rest } = resetCusEnt;
// let newCusEnt = rest as CustomerEntitlement;
let rolloverUpdate = getRolloverUpdates({
cusEnt,
nextResetAt: cusEnt.next_reset_at! as number,
@@ -248,10 +246,11 @@ export const resetCustomerEntitlement = async ({
// 1. Check if should reset
let nextResetAt = getNextResetAt(
new UTCDate(cusEnt.next_reset_at!),
cusEnt.entitlement.interval as EntInterval
);
let nextResetAt = getNextResetAt({
curReset: new UTCDate(cusEnt.next_reset_at!),
interval: cusEnt.entitlement.interval as EntInterval,
intervalCount: cusEnt.entitlement.interval_count,
});
let rolloverUpdate = getRolloverUpdates({
cusEnt,

View File

@@ -8,9 +8,6 @@ import { schemas as schema } from "@autumn/shared";
export let client = postgres(process.env.DATABASE_URL!);
export let db = drizzle(client, { schema });
// export const localClient = postgres("postgresql://postgres:postgres@localhost:54322/postgres")
// export const localDb = drizzle(localClient, { schema });
export const initDrizzle = (params?: { maxConnections?: number }) => {
let maxConnections = params?.maxConnections || 10;
const client = postgres(process.env.DATABASE_URL!, {
@@ -19,7 +16,6 @@ export const initDrizzle = (params?: { maxConnections?: number }) => {
const db = drizzle(client, {
schema,
// logger: process.env.NODE_ENV === "development",
});
return { db, client };

View File

@@ -1,4 +1,111 @@
import pino from "pino";
import { Writable } from "stream";
// Custom log formatter for Bun compatibility
const createDevLogStream = () => {
const colors = {
reset: "\x1b[0m",
bright: "\x1b[1m",
dim: "\x1b[2m",
red: "\x1b[31m",
green: "\x1b[32m",
yellow: "\x1b[33m",
blue: "\x1b[34m",
magenta: "\x1b[35m",
cyan: "\x1b[36m",
white: "\x1b[37m",
gray: "\x1b[90m",
bgRed: "\x1b[41m",
};
const levelColors: Record<number | string, string> = {
// Numeric levels
10: colors.gray, // trace
20: colors.blue, // debug
30: colors.green, // info
40: colors.yellow, // warn
50: colors.red, // error
60: colors.bgRed, // fatal
// String levels
TRACE: colors.gray,
DEBUG: colors.blue,
INFO: colors.green,
WARN: colors.yellow,
ERROR: colors.red,
FATAL: colors.bgRed,
};
const levelNames: Record<number | string, string> = {
// Numeric levels
10: "TRACE",
20: "DEBUG",
30: "INFO",
40: "WARN",
50: "ERROR",
60: "FATAL",
// String levels (pass through)
TRACE: "TRACE",
DEBUG: "DEBUG",
INFO: "INFO",
WARN: "WARN",
ERROR: "ERROR",
FATAL: "FATAL",
};
return new Writable({
write(chunk, encoding, callback) {
try {
const log = JSON.parse(chunk.toString());
const timestamp = new Date(log.time)
.toISOString()
.replace("T", " ")
.replace("Z", "");
const level = log.level;
const levelColor = levelColors[level] || colors.white;
const levelName =
levelNames[level] || (typeof level === "string" ? level : "UNKNOWN");
// Format the message
let message = log.msg || "";
// Add any additional fields (excluding standard pino fields)
const excludeFields = [
"time",
"level",
"msg",
"pid",
"hostname",
"res",
"statusCode",
"worker",
"context",
"req",
"data",
];
const additionalFields = Object.keys(log)
.filter((key) => !excludeFields.includes(key))
.reduce((acc, key) => {
acc[key] = log[key];
return acc;
}, {} as any);
if (Object.keys(additionalFields).length > 0) {
message += " " + JSON.stringify(additionalFields, null, 2);
}
// Format the final log line
const formattedLog = `${colors.gray}${timestamp}${colors.reset} ${levelColor}${colors.bright}${levelName}${colors.reset} ${message}\n`;
process.stdout.write(formattedLog);
callback();
} catch (error) {
// Fallback for malformed JSON
process.stdout.write(chunk);
callback();
}
},
});
};
export const initLogger = () => {
// Create separate streams for console and HyperDX
@@ -7,26 +114,7 @@ export const initLogger = () => {
if (process.env.NODE_ENV === "development") {
streams.push({
level: process.env.NODE_ENV === "development" ? "debug" : "info",
stream: pino.transport({
target: "pino-pretty",
options: {
colorize: true,
translateTime: "UTC:yyyy-mm-dd HH:MM:ss",
ignore: "pid,hostname,res,statusCode,worker,context,req",
customColors: {
default: "white",
60: "bgRed",
50: "red",
40: "yellow",
30: "green",
20: "blue",
10: "gray",
message: "reset",
greyMessage: "gray",
time: "darkGray",
},
},
}),
stream: createDevLogStream(),
});
}
@@ -55,7 +143,7 @@ export const initLogger = () => {
},
},
// Use multistream to send logs to multiple destinations
pino.multistream(streams),
pino.multistream(streams)
);
return logger;

View File

@@ -12,6 +12,8 @@ import {
} from "@autumn/shared";
import {
CancelParams,
CheckoutParams,
CheckoutResult,
CheckParams,
CheckResult,
Customer,
@@ -168,6 +170,16 @@ export class AutumnInt {
return data;
}
async checkout(params: CheckoutParams) {
// const data = await this.post(`/attach`, {
// customer_id: customerId,
// product_id: productId,
// options: toSnakeCase(options),
// });
const data = await this.post(`/checkout`, params);
return data as CheckoutResult;
}
async sendEvent({
customerId,

View File

@@ -32,6 +32,7 @@ const createLogMethod = (pinoMethod: any, logtailMethod?: any) => {
const strings = args
.filter((arg) => typeof arg === "string")
.map(rewriteAppPath);
const objects = args
.filter((arg) => typeof arg !== "string" && arg !== null)
.map((obj) => (obj instanceof Error ? rewriteErrorStack(obj) : obj));
@@ -44,7 +45,7 @@ const createLogMethod = (pinoMethod: any, logtailMethod?: any) => {
const errorObject = args.find((arg) => arg instanceof Error);
if (errorObject) {
message = rewriteAppPath(
errorObject.stack || errorObject.message || "Error occurred",
errorObject.stack || errorObject.message || "Error occurred"
);
}
}

View File

@@ -11,10 +11,11 @@ export interface ResendEmailProps {
body: string;
from: string;
fromEmail?: string;
replyTo?: string;
}
export const nameToEmail = (name: string) => {
return `${name.toLowerCase().replace(/\s+/g, ".")}@${process.env.RESEND_DOMAIN}`;
return `${name.toLowerCase().replace(/\s+/g, ".")}@hey.${process.env.RESEND_DOMAIN}`;
};
export const sendTextEmail = async ({
@@ -71,6 +72,7 @@ export const sendHtmlEmail = async ({
subject,
body,
fromEmail,
replyTo,
}: ResendEmailProps) => {
const resend = createResendCli();
@@ -83,5 +85,6 @@ export const sendHtmlEmail = async ({
to: to,
subject: subject,
html: body,
replyTo,
});
};

View File

@@ -94,7 +94,10 @@ export const createStripeMeteredPrice = async ({
currency: org.default_currency || "usd",
nickname: `Autumn Price (${feature!.name}) [Placeholder]`,
recurring: {
...(billingIntervalToStripe(price.config!.interval!) as any),
...(billingIntervalToStripe({
interval: price.config!.interval,
intervalCount: price.config!.interval_count,
}) as any),
meter: meter!.id,
usage_type: "metered",
},
@@ -105,7 +108,7 @@ export const createStripeMeteredPrice = async ({
export const arrearProratedToStripeTiers = (
price: Price,
entitlement: EntitlementWithFeature,
entitlement: EntitlementWithFeature
) => {
let usageConfig = structuredClone(price.config) as UsagePriceConfig;
@@ -161,7 +164,10 @@ export const createStripeArrearProrated = async ({
let recurringData = undefined;
if (price.config!.interval != BillingInterval.OneOff) {
recurringData = billingIntervalToStripe(price.config!.interval!);
recurringData = billingIntervalToStripe({
interval: price.config!.interval,
intervalCount: price.config!.interval_count,
});
}
const config = price.config as UsagePriceConfig;

View File

@@ -28,7 +28,10 @@ export const createStripeFixedPrice = async ({
unit_amount: amount,
currency: org.default_currency!,
recurring: {
...(billingIntervalToStripe(config.interval!) as any),
...(billingIntervalToStripe({
interval: config.interval,
intervalCount: config.interval_count,
}) as any),
},
nickname: `Autumn Price (Fixed)`,

View File

@@ -53,7 +53,7 @@ export const searchStripeMeter = async ({
logger.info(`Stripe meter list took ${end - start}ms`);
let stripeMeter = allStripeMeters.find(
(m) => m.event_name == eventName || m.id == meterId,
(m) => m.event_name == eventName || m.id == meterId
);
return stripeMeter;
@@ -87,7 +87,7 @@ export const getStripeMeter = async ({
createNew = true;
} else {
logger.info(
`✅ Found existing meter for ${product.name} - ${feature!.name}`,
`✅ Found existing meter for ${product.name} - ${feature!.name}`
);
return stripeMeter;
}
@@ -107,7 +107,7 @@ export const getStripeMeter = async ({
// IN ARREAR
export const priceToInArrearTiers = (
price: Price,
entitlement: Entitlement,
entitlement: Entitlement
) => {
let usageConfig = structuredClone(price.config) as UsagePriceConfig;
const tiers: any[] = [];
@@ -177,7 +177,7 @@ export const createStripeInArrearPrice = async ({
if (internalEntityId && !useCheckout) {
if (!curStripeProduct) {
logger.info(
`Creating stripe in arrear product for ${relatedEnt.feature.name} (internal entity ID exists!)`,
`Creating stripe in arrear product for ${relatedEnt.feature.name} (internal entity ID exists!)`
);
let stripeProduct = await stripeCli.products.create({
name: `${product.name} - ${feature!.name}`,
@@ -199,7 +199,7 @@ export const createStripeInArrearPrice = async ({
}
logger.info(
`Creating stripe in arrear price for ${relatedEnt.feature.name} (no internal entity ID)`,
`Creating stripe in arrear price for ${relatedEnt.feature.name} (no internal entity ID)`
);
if (!feature) {
@@ -223,7 +223,7 @@ export const createStripeInArrearPrice = async ({
const tiers = priceToInArrearTiers(
price,
getPriceEntitlement(price, entitlements),
getPriceEntitlement(price, entitlements)
);
let priceAmountData = {};
@@ -257,7 +257,10 @@ export const createStripeInArrearPrice = async ({
...priceAmountData,
currency: org.default_currency!,
recurring: {
...(billingIntervalToStripe(price.config!.interval!) as any),
...(billingIntervalToStripe({
interval: price.config!.interval,
intervalCount: price.config!.interval_count,
}) as any),
meter: meter!.id,
usage_type: "metered",
},

View File

@@ -71,7 +71,10 @@ export const createStripePrepaid = async ({
let recurringData = undefined;
if (price.config!.interval != BillingInterval.OneOff) {
recurringData = billingIntervalToStripe(price.config!.interval!);
recurringData = billingIntervalToStripe({
interval: price.config!.interval,
intervalCount: price.config!.interval_count,
});
}
const config = price.config as UsagePriceConfig;

View File

@@ -10,9 +10,7 @@ import {
Organization,
UsagePriceConfig,
BillingType,
FeatureOptions,
} from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
import Stripe from "stripe";
import { createStripeFixedPrice } from "./createStripeFixedPrice.js";
@@ -46,10 +44,7 @@ export const checkCurStripePrice = async ({
expand: ["product"],
});
if (
!stripePrice.active ||
!(stripePrice.product as Stripe.Product).active
) {
if (!stripePrice.active) {
stripePrice = null;
}
} catch (error) {
@@ -122,7 +117,12 @@ export const createStripePriceIFNotExist = async ({
billingType == BillingType.OneOff
) {
if (!stripePrice) {
logger.info("Creating stripe fixed price");
// logger.info("Creating stripe fixed price: ", {
// data: {
// price,
// stripePrice,
// },
// });
await createStripeFixedPrice({
db,
stripeCli,

View File

@@ -46,11 +46,11 @@ export const createStripeSubThroughInvoice = async ({
let subItems = items.filter(
(i: any, index: number) =>
prices[index].config!.interval !== BillingInterval.OneOff,
prices[index].config!.interval !== BillingInterval.OneOff
);
let invoiceItems = items.filter(
(i: any, index: number) =>
prices[index].config!.interval === BillingInterval.OneOff,
prices[index].config!.interval === BillingInterval.OneOff
);
try {

View File

@@ -7,6 +7,7 @@ export const checkKeyValid = async (apiKey: string) => {
// Call customers.list
const customers = await stripe.customers.list();
// const account = await stripe.accounts.retrieve();
// console.log("Account", account);
// return account;
@@ -15,7 +16,7 @@ export const checkKeyValid = async (apiKey: string) => {
export const createWebhookEndpoint = async (
apiKey: string,
env: AppEnv,
orgId: string,
orgId: string
) => {
const stripe = new Stripe(apiKey);

View File

@@ -23,27 +23,39 @@ export const createSubMeta = ({ features }: { features: Feature[] }) => {
return { usage_features: JSON.stringify(usageFeatures) };
};
export const billingIntervalToStripe = (interval: BillingInterval) => {
export const billingIntervalToStripe = ({
interval,
intervalCount,
}: {
interval: BillingInterval;
intervalCount?: number | null;
}) => {
const finalCount = intervalCount ?? 1;
switch (interval) {
case BillingInterval.Week:
return {
interval: "week",
interval_count: finalCount,
};
case BillingInterval.Month:
return {
interval: "month",
interval_count: 1,
interval_count: finalCount,
};
case BillingInterval.Quarter:
return {
interval: "month",
interval_count: 3,
interval_count: finalCount * 3,
};
case BillingInterval.SemiAnnual:
return {
interval: "month",
interval_count: 6,
interval_count: finalCount * 6,
};
case BillingInterval.Year:
return {
interval: "year",
interval_count: 1,
interval_count: finalCount,
};
default:
break;
@@ -104,10 +116,12 @@ export const getPlaceholderItem = ({
product,
org,
interval,
intervalCount,
}: {
product: FullProduct;
org: Organization;
interval: BillingInterval;
intervalCount: number;
}) => {
return {
price_data: {
@@ -115,7 +129,10 @@ export const getPlaceholderItem = ({
unit_amount: 0,
currency: org.default_currency || "usd",
recurring: {
...billingIntervalToStripe(interval as BillingInterval),
...billingIntervalToStripe({
interval,
intervalCount,
}),
},
},
quantity: 0,

View File

@@ -45,7 +45,7 @@ export const getStripeSubs = async ({
} catch (error: any) {
console.log(
`(warning) getStripeSubs: Failed to get sub ${subId}`,
error.message,
error.message
);
return null;
}
@@ -133,7 +133,7 @@ export const getUsageBasedSub = async ({
let autumnSub = autumnSubs?.find((sub) => sub.stripe_id == stripeSub.id);
if (autumnSub) {
let containsFeature = autumnSub.usage_features.includes(
feature.internal_id!,
feature.internal_id!
);
if (containsFeature) {
return stripeSub;
@@ -149,7 +149,7 @@ export const getUsageBasedSub = async ({
if (
!usageFeatures ||
usageFeatures.find(
(feat: any) => feat.internal_id == feature.internal_id,
(feat: any) => feat.internal_id == feature.internal_id
) === undefined
) {
continue;
@@ -179,15 +179,14 @@ export const getSubItemsForCusProduct = async ({
prices.some(
(p) =>
p.config?.stripe_price_id == item.price.id ||
(p.config as UsagePriceConfig).stripe_product_id ==
item.price.product,
(p.config as UsagePriceConfig).stripe_product_id == item.price.product
)
) {
subItems.push(item);
}
}
let otherSubItems = stripeSub.items.data.filter(
(item) => !subItems.some((i) => i.id == item.id),
(item) => !subItems.some((i) => i.id == item.id)
);
return { subItems, otherSubItems };
@@ -216,12 +215,17 @@ export const getStripeSchedules = async ({
}
const prices = await Promise.all(batchPricesGet);
const interval = prices[0].recurring?.interval;
const billingInterval = stripeToAutumnInterval({
interval: prices[0].recurring?.interval as string,
intervalCount: prices[0].recurring?.interval_count || 1,
});
// const billingInterval = stripeToAutumnInterval({
// interval: prices[0].recurring?.interval as string,
// intervalCount: prices[0].recurring?.interval_count || 1,
// });
return { schedule, interval: billingInterval, prices };
return {
schedule,
interval: interval as BillingInterval,
intervalCount: prices[0].recurring?.interval_count || 1,
prices,
};
} catch (error: any) {
console.log("Error getting stripe schedule.", error.message);
return null;
@@ -237,6 +241,7 @@ export const getStripeSchedules = async ({
return schedulesAndSubs.filter((schedule) => schedule !== null) as {
schedule: Stripe.SubscriptionSchedule;
interval: BillingInterval;
intervalCount: number;
prices: Stripe.Price[];
}[];
};

View File

@@ -25,6 +25,7 @@ export const createStripeSub = async ({
org,
freeTrial,
invoiceOnly = false,
finalizeInvoice = false,
anchorToUnix,
itemSet,
now,
@@ -36,6 +37,7 @@ export const createStripeSub = async ({
freeTrial: FreeTrial | null;
org: Organization;
invoiceOnly?: boolean;
finalizeInvoice?: boolean;
anchorToUnix?: number;
itemSet: ItemSet;
now?: number;
@@ -58,6 +60,7 @@ export const createStripeSub = async ({
? getAlignedIntervalUnix({
alignWithUnix: anchorToUnix,
interval: itemSet.interval,
intervalCount: itemSet.intervalCount,
now,
})
: undefined;
@@ -91,6 +94,12 @@ export const createStripeSub = async ({
coupon: reward ? reward.id : undefined,
});
if (invoiceOnly && finalizeInvoice) {
await stripeCli.invoices.finalizeInvoice(
subscription.latest_invoice as string
);
}
// Store
await SubService.createSub({
db,

View File

@@ -28,16 +28,21 @@ import {
import { isUsagePrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
import RecaseError from "@/utils/errorUtils.js";
import { logger } from "@/external/logtail/logtailUtils.js";
import {
intervalKeyToPrice,
priceToIntervalKey,
} from "@/internal/products/prices/priceUtils/convertPrice.js";
const getIntervalToPrices = (prices: Price[]) => {
const intervalToPrices: Record<string, Price[]> = {};
for (const price of prices) {
const interval = price.config.interval;
if (!intervalToPrices[interval]) {
intervalToPrices[interval] = [];
// const interval = price.config.interval;
const key = priceToIntervalKey(price);
if (!intervalToPrices[key]) {
intervalToPrices[key] = [];
}
intervalToPrices[interval].push(price);
intervalToPrices[key].push(price);
}
let oneOffPrices =
@@ -93,8 +98,8 @@ export const getStripeSubItems = async ({
const intervalToPrices = getIntervalToPrices(prices);
for (const interval in intervalToPrices) {
const prices = intervalToPrices[interval];
for (const intervalKey in intervalToPrices) {
const prices = intervalToPrices[intervalKey];
let subItems: any[] = [];
@@ -165,12 +170,14 @@ export const getStripeSubItems = async ({
subItems.push(lineItem);
}
const { interval, intervalCount } = intervalKeyToPrice(intervalKey);
if (subItems.length == 0) {
subItems.push(
...getArrearItems({
prices,
org,
interval: interval as BillingInterval,
intervalCount,
})
);
}
@@ -178,6 +185,7 @@ export const getStripeSubItems = async ({
itemSets.push({
items: subItems,
interval,
intervalCount,
subMeta: {
usage_features: JSON.stringify(usage_features),
},
@@ -186,7 +194,18 @@ export const getStripeSubItems = async ({
});
}
itemSets.sort((a, b) => compareBillingIntervals(a.interval, b.interval));
itemSets.sort((a, b) =>
compareBillingIntervals({
configA: {
interval: a.interval,
intervalCount: a.intervalCount,
},
configB: {
interval: b.interval,
intervalCount: b.intervalCount,
},
})
);
return itemSets;
};

View File

@@ -2,6 +2,7 @@ import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import {
BillingInterval,
BillingType,
intervalsDifferent,
Organization,
UsagePriceConfig,
} from "@autumn/shared";
@@ -13,15 +14,25 @@ export const getArrearItems = ({
prices,
org,
interval,
intervalCount,
}: {
prices: Price[];
interval: BillingInterval;
intervalCount: number;
org: Organization;
}) => {
let placeholderItems: any[] = [];
for (const price of prices) {
let billingType = getBillingType(price.config!);
if (price.config!.interval! != interval) {
if (
intervalsDifferent({
intervalA: {
interval: price.config!.interval!,
intervalCount: price.config!.interval_count!,
},
intervalB: { interval, intervalCount },
})
) {
continue;
}
@@ -33,7 +44,10 @@ export const getArrearItems = ({
unit_amount: 1,
currency: org.default_currency || "usd",
recurring: {
...billingIntervalToStripe(interval),
...billingIntervalToStripe({
interval,
intervalCount,
}),
},
},
quantity: 0,

View File

@@ -204,6 +204,7 @@ stripeWebhookRouter.post(
logger,
});
break;
case "subscription_schedule.canceled":
const canceledSchedule = event.data.object;
await handleSubscriptionScheduleCanceled({

View File

@@ -77,44 +77,55 @@ export const calculateMetered1Price = ({
export const subToAutumnInterval = (sub: Stripe.Subscription) => {
let recuringItem = sub.items.data.find((i) => i.price.recurring != null);
if (!recuringItem) {
return BillingInterval.OneOff;
return {
interval: BillingInterval.OneOff,
intervalCount: 1,
};
}
return stripeToAutumnInterval({
interval: recuringItem.price.recurring!.interval,
intervalCount: recuringItem.price.recurring!.interval_count,
});
return {
interval: recuringItem.price.recurring!.interval as BillingInterval,
intervalCount: recuringItem.price.recurring!.interval_count || 1,
};
// return stripeToAutumnInterval({
// interval: recuringItem.price.recurring!.interval,
// intervalCount: recuringItem.price.recurring!.interval_count,
// });
};
export const stripeToAutumnInterval = ({
interval,
intervalCount,
}: {
interval: string;
intervalCount: number;
}) => {
if (interval === "month" && intervalCount === 1) {
return BillingInterval.Month;
}
// export const stripeToAutumnInterval = ({
// interval,
// intervalCount,
// }: {
// interval: string;
// intervalCount: number;
// }) => {
// if (interval === "month" && intervalCount === 1) {
// return BillingInterval.Month;
// }
if (interval === "month" && intervalCount === 3) {
return BillingInterval.Quarter;
}
// // if (interval === "month" && intervalCount === 3) {
// // return BillingInterval.Quarter;
// // }
if (interval === "month" && intervalCount === 6) {
return BillingInterval.SemiAnnual;
}
// // if (interval === "month" && intervalCount === 6) {
// // return BillingInterval.SemiAnnual;
// // }
if (
(interval === "month" && intervalCount === 12) ||
(interval === "year" && intervalCount === 1)
) {
return BillingInterval.Year;
}
};
// if (
// (interval === "month" && intervalCount === 12) ||
// (interval === "year" && intervalCount === 1)
// ) {
// return BillingInterval.Year;
// }
// };
export const subItemToAutumnInterval = (item: Stripe.SubscriptionItem) => {
return stripeToAutumnInterval({
interval: item.price.recurring?.interval!,
intervalCount: item.price.recurring?.interval_count!,
});
return {
interval: item.price.recurring?.interval as BillingInterval,
intervalCount: item.price.recurring?.interval_count || 1,
};
// return stripeToAutumnInterval({
// interval: item.price.recurring?.interval!,
// intervalCount: item.price.recurring?.interval_count!,
// });
};

View File

@@ -97,8 +97,9 @@ export const handleCheckoutSub = async ({
getArrearItems({
prices: attachParams.prices,
interval: attachParams.itemSets?.[0]?.interval,
intervalCount: attachParams.itemSets?.[0]?.intervalCount,
org,
}),
})
);
}

View File

@@ -39,6 +39,7 @@ const filterUsagePrices = ({
prices: attachParams.prices,
interval: itemSet.interval,
org: attachParams.org,
intervalCount: itemSet.intervalCount,
});
}
@@ -89,17 +90,6 @@ export const handleRemainingSets = async ({
itemSet.items = filteredItems;
// itemSet.items =
// filteredItems.length == 0
// ? attachParams.products.map((p: FullProduct) =>
// getPlaceholderItem({
// product: p,
// org,
// interval: itemSet.interval,
// }),
// )
// : filteredItems;
const subscription = (await createStripeSub({
db,
stripeCli,

View File

@@ -1,32 +1,21 @@
import { clearOrgCache } from "@/internal/orgs/orgUtils/clearOrgCache.js";
import { createSupabaseClient } from "../supabaseUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { safeSb } from "./safeSb.js";
import { DrizzleCli, client } from "@/db/initDrizzle.js";
export const subscribeToOrgUpdates = safeSb({
fn: ({ db }: { db: DrizzleCli }) => {
try {
const sb = createSupabaseClient();
sb.channel("table-db-changes")
.on(
"postgres_changes",
{
event: "UPDATE",
schema: "public",
table: "organizations",
},
async (payload) => {
try {
await clearOrgCache({ db, orgId: payload.new.id });
} catch (error) {
console.warn("Error clearing org cache:", error);
}
},
)
.subscribe();
} catch (error) {
console.warn("Error subscribing to org updates:", error);
}
},
action: "subscribe to org updates",
});
export const subscribeToOrgUpdates = async ({ db }: { db: DrizzleCli }) => {
try {
await client.listen("org_updates", async (payload) => {
try {
const data = JSON.parse(payload);
if (data.table === "organizations" && data.operation === "UPDATE") {
await clearOrgCache({ db, orgId: data.new.id });
}
} catch (error) {
console.warn("Error processing org update notification:", error);
}
});
console.log("Successfully subscribed to organization updates via PostgreSQL LISTEN/NOTIFY");
} catch (error) {
console.warn("Error subscribing to org updates:", error);
}
};

View File

@@ -30,6 +30,7 @@ import { ClickHouseManager } from "./external/clickhouse/ClickHouseManager.js";
const tracer = trace.getTracer("express");
checkEnvVars();
// subscribeToOrgUpdates({ db });
const init = async () => {
const app = express();
@@ -45,8 +46,6 @@ const init = async () => {
"https://staging.useautumn.com",
"https://*.useautumn.com",
"https://localhost:8080",
"https://app.aidvize.com",
"http://staging.aidvize.com",
"https://www.alphalog.ai",
"https://*.alphalog.ai",
process.env.CLIENT_URL || "",
@@ -84,8 +83,6 @@ const init = async () => {
await CacheManager.getInstance();
await ClickHouseManager.getInstance();
subscribeToOrgUpdates({ db });
app.use(async (req: any, res: any, next: any) => {
req.env = req.env = req.headers["app_env"] || AppEnv.Sandbox;
req.db = db;
@@ -180,7 +177,7 @@ if (process.env.NODE_ENV === "development") {
console.log(`Master ${process.pid} is running`);
console.log("Number of CPUs", numCPUs);
let numWorkers = 10;
let numWorkers = 7;
for (let i = 0; i < numWorkers; i++) {
cluster.fork();

View File

@@ -72,6 +72,14 @@ export const getEntityResponse = async ({
skipCache,
});
if (!customer) {
throw new RecaseError({
message: `Customer ${customerId} not found`,
code: ErrCode.CustomerNotFound,
statusCode: 400,
});
}
let entities = customer.entities.filter((e: Entity) =>
entityIds.includes(e.id)
);

View File

@@ -118,6 +118,7 @@ export const getOptions = ({
anchorToUnix,
proration,
interval: (i.interval || BillingInterval.OneOff) as BillingInterval,
intervalCount: i.interval_count || 1,
now,
});
@@ -139,7 +140,7 @@ export const getOptions = ({
}
const currentOptions = cusProduct?.options.find(
(o) => o.feature_id == i.feature_id,
(o) => o.feature_id == i.feature_id
);
let currentQuantity = currentOptions?.quantity;

View File

@@ -21,6 +21,7 @@ import { getEventTimestamp } from "./eventUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { runUpdateUsageTask } from "@/trigger/updateUsageTask.js";
import { logger } from "@/external/logtail/logtailUtils.js";
import { isPaidContinuousUse } from "@/internal/features/featureUtils.js";
export const eventsRouter: Router = Router();
export const usageRouter: Router = Router();
@@ -201,9 +202,8 @@ export const handleUsageEvent = async ({
entityId: entity_id,
};
const featureUsageType = feature.config?.usage_type;
// console.log(`Feature Usage Type: ${featureUsageType}`);
if (featureUsageType === FeatureUsageType.Continuous) {
if (isPaidContinuousUse({ feature, fullCus: customer })) {
console.log(`Running update usage task synchronously`);
await runUpdateUsageTask({
payload,
logger: console,

View File

@@ -61,7 +61,12 @@ export const handleCreateCheckout = async ({
});
let billingCycleAnchorUnixSeconds = org.config.anchor_start_of_month
? Math.floor(getNextStartOfMonthUnix(itemSets[0].interval) / 1000)
? Math.floor(
getNextStartOfMonthUnix({
interval: itemSets[0].interval,
intervalCount: itemSets[0].intervalCount,
}) / 1000
)
: undefined;
if (attachParams.billingAnchor) {

View File

@@ -12,7 +12,10 @@ import { priceToProduct } from "@/internal/products/prices/priceUtils/findPriceU
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
import { AttachConfig } from "@autumn/shared";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js";
import {
attachToInvoiceResponse,
insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { Decimal } from "decimal.js";
import { isFixedPrice } from "@/internal/products/prices/priceUtils/usagePriceUtils/classifyUsagePrice.js";
@@ -160,7 +163,7 @@ export const handleOneOffFunction = async ({
attachParams: attachToInsertParams(attachParams, product),
lastInvoiceId: stripeInvoice.id,
logger,
}),
})
);
}
await Promise.all(batchInsert);
@@ -180,12 +183,14 @@ export const handleOneOffFunction = async ({
AttachResultSchema.parse({
success: true,
message: `Successfully purchased ${productNames} and attached to ${customerName}`,
invoice: invoiceOnly ? stripeInvoice : undefined,
invoice: invoiceOnly
? attachToInvoiceResponse({ invoice: stripeInvoice })
: undefined,
code: SuccessCode.OneOffProductAttached,
product_ids: products.map((p) => p.id),
customer_id: customer.id || customer.internal_id,
scenario: AttachScenario.New,
}),
})
);
}
};

View File

@@ -8,7 +8,10 @@ import {
AttachParams,
AttachResultSchema,
} from "@/internal/customers/cusProducts/AttachParams.js";
import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js";
import {
attachToInvoiceResponse,
insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { getNextStartOfMonthUnix } from "@/internal/products/prices/billingIntervalUtils.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import RecaseError from "@/utils/errorUtils.js";
@@ -20,6 +23,7 @@ import {
AttachScenario,
BillingInterval,
ErrCode,
intervalsDifferent,
SuccessCode,
} from "@autumn/shared";
import Stripe from "stripe";
@@ -78,15 +82,28 @@ export const handlePaidProduct = async ({
continue;
}
let mergeWithSub = mergeSubs.find(
(sub) => subToAutumnInterval(sub) == itemSet.interval
);
let mergeWithSub = mergeSubs.find((sub) => {
let subInterval = subToAutumnInterval(sub);
return !intervalsDifferent({
intervalA: {
interval: subInterval.interval,
intervalCount: subInterval.intervalCount,
},
intervalB: {
interval: itemSet.interval,
intervalCount: itemSet.intervalCount,
},
});
});
let subscription;
try {
let billingCycleAnchorUnix;
if (org.config.anchor_start_of_month) {
billingCycleAnchorUnix = getNextStartOfMonthUnix(itemSet.interval);
billingCycleAnchorUnix = getNextStartOfMonthUnix({
interval: itemSet.interval,
intervalCount: itemSet.intervalCount,
});
}
if (attachParams.billingAnchor) {
@@ -105,6 +122,7 @@ export const handlePaidProduct = async ({
freeTrial,
invoiceOnly,
itemSet,
finalizeInvoice: attachParams.finalizeInvoice,
anchorToUnix: billingCycleAnchorUnix,
reward: i == 0 ? reward : undefined,
now: attachParams.now,
@@ -179,7 +197,9 @@ export const handlePaidProduct = async ({
code: SuccessCode.NewProductAttached,
product_ids: products.map((p) => p.id),
customer_id: customer.id || customer.internal_id,
invoice: invoiceOnly ? invoices?.[0] : undefined,
invoice: invoiceOnly
? attachToInvoiceResponse({ invoice: invoices?.[0] })
: undefined,
})
);
} else {

View File

@@ -29,7 +29,7 @@ export const handleScheduleFunction = async ({
}) => {
const logger = req.logtail;
const product = attachParams.products[0];
const { stripeCli } = attachParams;
const { stripeCli, customer: fullCus } = attachParams;
const { curMainProduct, curScheduledProduct } = attachParamToCusProducts({
attachParams,
@@ -55,7 +55,9 @@ export const handleScheduleFunction = async ({
// 3. Get schedules for current cus products
logger.info(`3. Getting schedules for current cus products`);
let schedules = await cusProductsToSchedules({
cusProducts: [curMainProduct, curScheduledProduct],
// cusProducts: [curMainProduct, curScheduledProduct],
// cusProducts: [curMainProduct, curScheduledProduct],
cusProducts: fullCus.customer_products,
stripeCli,
});
@@ -69,7 +71,9 @@ export const handleScheduleFunction = async ({
const stripeSchedules: Stripe.SubscriptionSchedule[] = [];
for (const itemSet of itemSets) {
let scheduleObj = schedules.find(
(schedule) => schedule.interval === itemSet.interval,
(schedule) =>
schedule.interval === itemSet.interval &&
schedule.intervalCount === itemSet.intervalCount
);
// If schedule exists, update it
@@ -110,7 +114,7 @@ export const handleScheduleFunction = async ({
cusProductId: curCusProduct.id,
updates: {
scheduled_ids: curCusProduct.scheduled_ids?.filter(
(id) => !newScheduledIds.includes(id),
(id) => !newScheduledIds.includes(id)
),
},
});
@@ -147,7 +151,7 @@ export const handleScheduleFunction = async ({
product_ids: [product.id],
customer_id:
attachParams.customer.id || attachParams.customer.internal_id,
}),
})
);
} else {
res.status(200).json({

View File

@@ -17,6 +17,7 @@ import {
UsagePriceConfig,
BillingType,
BillingInterval,
intervalsDifferent,
} from "@autumn/shared";
import Stripe from "stripe";
@@ -28,6 +29,7 @@ export const getUsageInvoiceItems = async ({
cusProduct,
stripeSubs,
interval,
intervalCount,
}: {
db: DrizzleCli;
logger: any;
@@ -35,6 +37,7 @@ export const getUsageInvoiceItems = async ({
cusProduct: FullCusProduct;
stripeSubs: Stripe.Subscription[];
interval?: BillingInterval;
intervalCount?: number;
}) => {
const { stripeCli, org } = attachParams;
@@ -72,7 +75,14 @@ export const getUsageInvoiceItems = async ({
});
if (!sub) continue;
if (interval && interval !== subToAutumnInterval(sub)) continue;
if (
interval &&
intervalsDifferent({
intervalA: { interval, intervalCount },
intervalB: subToAutumnInterval(sub),
})
)
continue;
cusEntIds.push(cusEnt.id);
@@ -106,6 +116,7 @@ export const createUsageInvoiceItems = async ({
invoiceId,
logger,
interval,
intervalCount,
}: {
db: DrizzleCli;
attachParams: AttachParams;
@@ -114,6 +125,7 @@ export const createUsageInvoiceItems = async ({
invoiceId?: string;
logger: any;
interval?: BillingInterval;
intervalCount?: number;
}) => {
const { stripeCli } = attachParams;
@@ -123,6 +135,7 @@ export const createUsageInvoiceItems = async ({
cusProduct,
stripeSubs,
interval,
intervalCount,
logger,
});
@@ -131,7 +144,7 @@ export const createUsageInvoiceItems = async ({
const invoiceItem = invoiceItems[i];
const createInvoiceItem = async () => {
logger.info(
`🌟 Creating usage invoice item: ${invoiceItem.description}, amount: ${invoiceItem.price_data.unit_amount}`,
`🌟 Creating usage invoice item: ${invoiceItem.description}, amount: ${invoiceItem.price_data.unit_amount}`
);
await stripeCli.invoiceItems.create({
@@ -171,7 +184,7 @@ export const resetUsageBalances = async ({
});
let index = cusProduct.customer_entitlements.findIndex(
(ce) => ce.id === cusEntId,
(ce) => ce.id === cusEntId
);
cusProduct.customer_entitlements[index] = {

View File

@@ -18,7 +18,10 @@ import {
import { createFullCusProduct } from "@/internal/customers/add-product/createFullCusProduct.js";
import { attachToInsertParams } from "@/internal/products/productUtils.js";
import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js";
import {
attachToInvoiceResponse,
insertInvoiceFromAttach,
} from "@/internal/invoices/invoiceUtils.js";
import { updateSubsDiffInt } from "./updateSubsDiffInt.js";
export const handleUpgradeDiffInterval = async ({
@@ -69,7 +72,7 @@ export const handleUpgradeDiffInterval = async ({
cusProductId: curCusProduct.id,
updates: {
subscription_ids: curCusProduct.subscription_ids!.filter(
(subId) => subId !== newSubs[0].id,
(subId) => subId !== newSubs[0].id
),
processor: {
type: ProcessorType.Stripe,
@@ -106,7 +109,7 @@ export const handleUpgradeDiffInterval = async ({
attachParams,
invoiceId,
logger,
}),
})
);
}
@@ -124,8 +127,10 @@ export const handleUpgradeDiffInterval = async ({
product_ids: products.map((p) => p.id),
code: SuccessCode.UpgradedToNewProduct,
message: `Successfully upgraded from ${curProductName} to ${product.name}`,
invoice: config.invoiceOnly ? invoice : undefined,
}),
invoice: config.invoiceOnly
? attachToInvoiceResponse({ invoice: invoice || undefined })
: undefined,
})
);
} else {
res.status(200).json({

View File

@@ -4,7 +4,11 @@ import { updateScheduledSubWithNewItems } from "@/internal/customers/change-prod
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
import { FullCusProduct } from "@autumn/shared";
import {
FullCusProduct,
intervalsDifferent,
intervalsSame,
} from "@autumn/shared";
import Stripe from "stripe";
import { attachParamToCusProducts } from "../../attachUtils/convertAttachParams.js";
@@ -39,7 +43,7 @@ export const updateCurSchedules = async ({
});
for (const scheduleObj of schedules) {
const { interval, schedule } = scheduleObj;
const { interval, intervalCount, schedule } = scheduleObj;
// If schedule has passed, skip this step.
let phase = schedule.phases.length > 0 ? schedule.phases[0] : null;
@@ -51,7 +55,16 @@ export const updateCurSchedules = async ({
}
// Get corresponding item set
const itemSet = itemSets.find((itemSet) => itemSet.interval === interval);
const itemSet = itemSets.find((itemSet) =>
intervalsSame({
intervalA: { interval, intervalCount },
intervalB: {
interval: itemSet.interval,
intervalCount: itemSet.intervalCount,
},
})
);
if (!itemSet) {
continue;
}

View File

@@ -11,6 +11,7 @@ import { ExtendedRequest } from "@/utils/models/Request.js";
import { updateSubsByInt } from "./updateSubsSameInt.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { formatUnixToDate } from "@/utils/genUtils.js";
import { attachToInvoiceResponse } from "@/internal/invoices/invoiceUtils.js";
export const handleUpgradeSameInterval = async ({
req,
@@ -40,7 +41,7 @@ export const handleUpgradeSameInterval = async ({
}
logger.info(`1. Updating subs by interval`);
const { replaceables } = await updateSubsByInt({
const { replaceables, invoices } = await updateSubsByInt({
req,
curCusProduct: curCusProduct!,
attachParams,
@@ -81,9 +82,15 @@ export const handleUpgradeSameInterval = async ({
AttachResultSchema.parse({
customer_id: attachParams.customer.id,
product_ids: attachParams.products.map((p) => p.id),
// invoice: attachParams.invoiceOnly
// ? attachToInvoiceResponse({ invoice: invoices?.[0] })
// : undefined,
invoice: attachParams.invoiceOnly
? attachToInvoiceResponse({ invoice: invoices?.[0] || undefined })
: undefined,
code: "updated_product_successfully",
message: `Successfully updated product`,
}),
})
);
} else {
res.status(200).json({

View File

@@ -3,7 +3,12 @@ import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSub
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { AttachConfig, FullCusProduct, Replaceable } from "@autumn/shared";
import {
AttachConfig,
FullCusProduct,
intervalsSame,
Replaceable,
} from "@autumn/shared";
import { addSubItemsToRemove } from "../attachFuncUtils.js";
import { updateStripeSub } from "../../attachUtils/updateStripeSub/updateStripeSub.js";
import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js";
@@ -51,9 +56,16 @@ export const updateSubsByInt = async ({
// const replaceables: Replaceable[] = [];
for (const sub of stripeSubs) {
let interval = subToAutumnInterval(sub);
// let interval = subToAutumnInterval(sub);
let subInterval = subToAutumnInterval(sub);
let itemSet = itemSets.find((itemSet) => {
return intervalsSame({
intervalA: itemSet,
intervalB: subInterval,
});
})!;
let itemSet = itemSets.find((itemSet) => itemSet.interval === interval)!;
await addSubItemsToRemove({
sub,
cusProduct: curCusProduct,
@@ -67,14 +79,17 @@ export const updateSubsByInt = async ({
stripeSubs: [sub],
itemSet,
logger,
interval,
interval: itemSet.interval,
intervalCount: itemSet.intervalCount,
});
if (latestInvoice) {
invoices.push(latestInvoice);
}
logger.info(`Updated sub ${sub.id}, interval ${interval}`);
logger.info(
`Updated sub ${sub.id}, interval ${itemSet.interval}, intervalCount ${itemSet.intervalCount}`
);
}
const batchInvUpdate = [];
@@ -85,9 +100,9 @@ export const updateSubsByInt = async ({
attachParams,
stripeInvoice: invoice,
logger,
}),
})
);
}
return { replaceables };
return { replaceables, invoices };
};

View File

@@ -66,7 +66,8 @@ export const getAttachParams = async ({
// Others
apiVersion,
successUrl: attachBody.success_url,
invoiceOnly: attachBody.invoice_only,
invoiceOnly: attachBody.invoice_only || attachBody.invoice,
finalizeInvoice: attachBody.invoice,
billingAnchor: attachBody.billing_cycle_anchor,
metadata: attachBody.metadata,
disableFreeTrial: attachBody.free_trial === false || false,

View File

@@ -1,5 +1,8 @@
import { AttachParams } from "@/internal/customers/cusProducts/AttachParams.js";
import { getFirstInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import {
getLargestInterval,
intervalsDifferent,
} from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import Stripe from "stripe";
import { attachParamsToProduct } from "./convertAttachParams.js";
@@ -12,7 +15,10 @@ export const getCycleWillReset = ({
stripeSubs: Stripe.Subscription[];
}) => {
const product = attachParamsToProduct({ attachParams });
const firstInterval = getFirstInterval({ prices: product.prices });
const firstInterval = getLargestInterval({ prices: product.prices });
const prevInterval = subToAutumnInterval(stripeSubs[0]);
return prevInterval !== firstInterval;
return intervalsDifferent({
intervalA: firstInterval,
intervalB: prevInterval,
});
};

View File

@@ -273,6 +273,7 @@ const getChangeProductBranch = async ({
// }
let isUpgrade = isProductUpgrade({ prices1: curPrices, prices2: newPrices });
if (isUpgrade) {
if (isTrialing(curMainProduct!)) {
return AttachBranch.MainIsTrial;

View File

@@ -1,6 +1,11 @@
import { AttachParams } from "../../cusProducts/AttachParams.js";
import { AttachFlags } from "../models/AttachFlags.js";
import { AttachConfig, AttachBranch } from "@autumn/shared";
import {
AttachConfig,
AttachBranch,
intervalsSame,
intervalToValue,
} from "@autumn/shared";
import { AttachBody } from "@autumn/shared";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { nullish } from "@/utils/genUtils.js";
@@ -27,12 +32,66 @@ export const intervalsAreSame = ({
let newProduct = attachParamsToProduct({ attachParams });
let curPrices = cusProductToPrices({ cusProduct: curCusProduct! });
let curIntervals = new Set(curPrices.map((p) => p.config.interval));
let newIntervals = new Set(newProduct.prices.map((p) => p.config.interval));
const curIntervals = new Set(
curPrices.map((p) =>
intervalToValue(p.config.interval, p.config.interval_count)
)
);
const newIntervals = new Set(
newProduct.prices.map((p) =>
intervalToValue(p.config.interval, p.config.interval_count)
)
);
return (
curIntervals.size === newIntervals.size &&
[...curIntervals].every((interval) => newIntervals.has(interval))
);
for (const price of curPrices) {
let hasSimilarInterval = newProduct.prices.some((p) => {
return intervalsSame({
intervalA: price.config,
intervalB: p.config,
});
});
if (!hasSimilarInterval) {
return false;
}
}
for (const price of newProduct.prices) {
let hasSimilarInterval = curPrices.some((p) => {
return intervalsSame({
intervalA: price.config,
intervalB: p.config,
});
});
if (!hasSimilarInterval) {
return false;
}
}
return true;
// let curIntervals = new Set(
// curPrices.map((p) => ({
// interval: p.config.interval,
// intervalCount: p.config.interval_count,
// }))
// );
// let newIntervals = new Set(
// newProduct.prices.map((p) => ({
// interval: p.config.interval,
// intervalCount: p.config.interval_count,
// }))
// );
// return (
// curIntervals.size === newIntervals.size &&
// [...curIntervals].every((interval) => newIntervals.has(interval))
// );
};
export const getAttachConfig = async ({
@@ -51,7 +110,7 @@ export const getAttachConfig = async ({
let flags: AttachFlags = {
isPublic: req.isPublic,
forceCheckout: attachBody.force_checkout || false,
invoiceOnly: attachBody.invoice_only || false,
invoiceOnly: attachParams.invoiceOnly || false,
isFree: isFreeProduct(prices),
noPaymentMethod: nullish(paymentMethod) ? true : false,
};

View File

@@ -4,6 +4,8 @@ import {
BillingType,
FullCusProduct,
FullProduct,
intervalsDifferent,
intervalsSame,
} from "@autumn/shared";
import Stripe from "stripe";
import { getContUseInvoiceItems } from "./getContUseInvoiceItems.js";
@@ -35,7 +37,7 @@ export const filterContUsageProrations = async ({
subscription: sub.id,
});
const interval = subToAutumnInterval(sub);
const intervalSet = subToAutumnInterval(sub);
for (const item of upcomingLines.data) {
if (!item.proration) continue;
@@ -48,12 +50,12 @@ export const filterContUsageProrations = async ({
if (!price) continue;
logger.info(
`Deleting ii: ${item.description} - ${item.amount / 100} (${interval})`,
`Deleting ii: ${item.description} - ${item.amount / 100} (${intervalSet.interval}, ${intervalSet.intervalCount})`
);
await stripeCli.invoiceItems.del(
// @ts-ignore -- Stripe types are not correct
item.parent.subscription_item_details.invoice_item,
item.parent.subscription_item_details.invoice_item
);
}
};
@@ -64,12 +66,14 @@ export const createAndFilterContUseItems = async ({
stripeSubs,
logger,
interval,
intervalCount,
}: {
attachParams: AttachParams;
curMainProduct: FullCusProduct;
stripeSubs: Stripe.Subscription[];
logger: any;
interval?: BillingInterval;
intervalCount?: number;
}) => {
const { stripeCli, customer, org } = attachParams;
const product = attachParamsToProduct({ attachParams });
@@ -88,8 +92,13 @@ export const createAndFilterContUseItems = async ({
});
let sub =
stripeSubs.find((sub) => subToAutumnInterval(sub) == interval) ||
stripeSubs[0];
stripeSubs.find((sub) => {
return intervalsSame({
intervalA: { interval: interval!, intervalCount: intervalCount! },
intervalB: subToAutumnInterval(sub),
});
// subToAutumnInterval(sub) == interval
}) || stripeSubs[0];
await filterContUsageProrations({
sub,
@@ -113,12 +122,20 @@ export const createAndFilterContUseItems = async ({
product.prices.find((p) => p.id === item.price_id) ||
curPrices.find((p) => p.id === item.price_id);
if (interval && price?.config.interval !== interval) {
if (
interval &&
price?.config &&
intervalsDifferent({
// price?.config.interval !== interval
intervalA: price?.config,
intervalB: { interval, intervalCount },
})
) {
continue;
}
logger.info(
`Adding invoice item: ${item.description}, ${item.description}, interval: ${interval}`,
`Adding invoice item: ${item.description}, ${item.description}, interval: ${interval}`
);
await stripeCli.invoiceItems.create({

View File

@@ -7,6 +7,7 @@ import {
FullCustomerEntitlement,
FullEntitlement,
getFeatureInvoiceDescription,
intervalsSame,
PreviewLineItem,
Price,
} from "@autumn/shared";
@@ -121,7 +122,7 @@ export const getContUseInvoiceItems = async ({
const cusEnts = cusProduct ? cusProduct.customer_entitlements : [];
const product = attachParamsToProduct({ attachParams });
const intervalsSame = intervalsAreSame({ attachParams });
const allIntervalsSame = intervalsAreSame({ attachParams });
const curItems = stripeSubs
? await getCurContUseItems({
stripeSubs,
@@ -150,7 +151,7 @@ export const getContUseInvoiceItems = async ({
? getRelatedCusPrice(prevCusEnt, cusPrices)!
: undefined;
if (!intervalsSame || !prevCusEnt || !stripeSubs) {
if (!allIntervalsSame || !prevCusEnt || !stripeSubs) {
const newItem = await getContUseNewItems({
price,
ent,
@@ -159,7 +160,7 @@ export const getContUseInvoiceItems = async ({
});
const prevItem = curItems.find(
(item) => item.price_id === prevCusPrice?.price.id,
(item) => item.price_id === prevCusPrice?.price.id
);
newItems.push(newItem);
@@ -172,12 +173,16 @@ export const getContUseInvoiceItems = async ({
}
const curItem = curItems.find(
(item) => item.price_id === prevCusPrice?.price.id,
(item) => item.price_id === prevCusPrice?.price.id
);
let sub = stripeSubs!.find(
(sub) => subToAutumnInterval(sub) === price.config.interval,
);
let sub = stripeSubs!.find((sub) => {
let subInterval = subToAutumnInterval(sub);
return intervalsSame({
intervalA: price.config,
intervalB: subInterval,
});
});
let {
oldItem,

View File

@@ -213,7 +213,6 @@ export const handleAttachErrors = async ({
AttachBranch.NewVersion,
AttachBranch.SameCustom,
AttachBranch.UpdatePrepaidQuantity,
AttachBranch.Renew,
];
if (updateProductFlows.includes(branch)) {
handleNonCheckoutErrors({

View File

@@ -1,5 +1,5 @@
import Stripe from "stripe";
import { BillingInterval, AttachConfig } from "@autumn/shared";
import { BillingInterval, AttachConfig, intervalsSame } from "@autumn/shared";
import { ProrationBehavior } from "@autumn/shared";
import { SubService } from "@/internal/subscriptions/SubService.js";
import { ItemSet } from "@/utils/models/ItemSet.js";
@@ -23,7 +23,7 @@ export const getSubAndInvoiceItems = async ({
let subItems = items.filter(
(i: any, index: number) =>
i.deleted || prices[index].config!.interval !== BillingInterval.OneOff,
i.deleted || prices[index].config!.interval !== BillingInterval.OneOff
);
let addInvoiceItems = items.filter((i: any, index: number) => {
@@ -48,6 +48,7 @@ export const updateStripeSub = async ({
itemSet,
logger,
interval,
intervalCount,
}: {
db: DrizzleCli;
attachParams: AttachParams;
@@ -58,6 +59,7 @@ export const updateStripeSub = async ({
shouldPreview?: boolean;
logger: any;
interval?: BillingInterval;
intervalCount?: number;
}) => {
const { curMainProduct } = attachParamToCusProducts({ attachParams });
const { stripeCli, customer, org, paymentMethod } = attachParams;
@@ -69,7 +71,13 @@ export const updateStripeSub = async ({
const curSub =
(interval
? stripeSubs.find((s) => subToAutumnInterval(s) === interval)
? stripeSubs.find((s) => {
let subInterval = subToAutumnInterval(s);
return intervalsSame({
intervalA: { interval, intervalCount },
intervalB: subInterval,
});
})
: stripeSubs[0]) || stripeSubs[0];
// 1. Update subscription
@@ -102,6 +110,16 @@ export const updateStripeSub = async ({
};
}
// if (invoiceOnly && attachParams.finalizeInvoice) {
// try {
// await stripeCli.invoices.finalizeInvoice(latestInvoice?.id as string);
// } catch (error) {
// logger.error(`Failed to finalize invoice ${latestInvoice?.id}`, {
// error,
// });
// }
// }
// 2. Create prorations for single use items
let { invoiceItems, cusEntIds } = await createUsageInvoiceItems({
db,
@@ -110,6 +128,7 @@ export const updateStripeSub = async ({
stripeSubs,
logger,
interval: config.sameIntervals ? interval : undefined,
intervalCount: config.sameIntervals ? intervalCount : undefined,
});
// 3. Create prorations for continuous use items
@@ -118,6 +137,7 @@ export const updateStripeSub = async ({
curMainProduct: curMainProduct!,
stripeSubs,
interval,
intervalCount,
logger,
});
@@ -145,6 +165,19 @@ export const updateStripeSub = async ({
env: customer.env,
});
if (invoiceOnly && attachParams.finalizeInvoice) {
logger.info(`FINALIZING INVOICE ${latestInvoice?.id}`);
try {
latestInvoice = await stripeCli.invoices.finalizeInvoice(
latestInvoice?.id as string
);
} catch (error) {
logger.error(`Failed to finalize invoice ${latestInvoice?.id}`, {
error,
});
}
}
return {
updatedSub,
latestInvoice: latestInvoice,

View File

@@ -1,5 +1,4 @@
import {
AttachBranch,
AttachFunction,
AttachScenario,
CheckoutResponseSchema,
@@ -108,17 +107,18 @@ export const handleCheckout = (req: any, res: any) =>
handler: async (req: ExtendedRequest, res: ExtendedResponse) => {
const { logger, features } = req;
const attachBody = AttachBodySchema.parse(req.body);
// Pre-populate options...
const { attachParams, flags, branch, config, func } = await getAttachVars(
{ req, attachBody }
);
const { attachParams, branch, func } = await getAttachVars({
req,
attachBody,
});
await getCheckoutOptions({
req,
attachParams,
});
let checkoutUrl = null;
if (func == AttachFunction.CreateCheckout) {
await checkStripeConnections({
req,
@@ -134,22 +134,24 @@ export const handleCheckout = (req: any, res: any) =>
returnCheckout: true,
});
const customer = attachParams.customer;
res.status(200).json(
CheckoutResponseSchema.parse({
url: checkout?.url,
customer_id: customer.id || customer.internal_id,
scenario: AttachScenario.New,
lines: [],
product: await getProductResponse({
product: attachParams.products[0],
features: features,
withDisplay: false,
options: attachParams.optionsList,
}),
})
);
return;
checkoutUrl = checkout?.url;
// const customer = attachParams.customer;
// res.status(200).json(
// CheckoutResponseSchema.parse({
// url: checkout?.url,
// customer_id: customer.id || customer.internal_id,
// scenario: AttachScenario.New,
// lines: [],
// product: await getProductResponse({
// product: attachParams.products[0],
// features: features,
// withDisplay: false,
// options: attachParams.optionsList,
// }),
// })
// );
// return;
}
const preview = await attachParamsToPreview({
@@ -175,6 +177,7 @@ export const handleCheckout = (req: any, res: any) =>
res.status(200).json({
...checkoutRes,
url: checkoutUrl,
options: attachParams.optionsList.map((o) => ({
quantity: o.quantity,
feature_id: o.feature_id,

View File

@@ -108,10 +108,6 @@ export const previewToCheckoutRes = async ({
return acc;
}
// if (item.interval !== newProduct.properties?.interval_group) {
// return acc;
// }
if (isPriceItem(item)) {
return acc.plus(item.price || 0);
}

View File

@@ -6,7 +6,7 @@ import { getAttachParams } from "./attachUtils/attachParams/getAttachParams.js";
import { getAttachBranch } from "./attachUtils/getAttachBranch.js";
import { getAttachConfig } from "./attachUtils/getAttachConfig.js";
import { handleAttachErrors } from "./attachUtils/handleAttachErrors.js";
import { checkStripeConnections, createStripePrices } from "./attachRouter.js";
import { checkStripeConnections } from "./attachRouter.js";
import { insertCustomItems } from "./attachUtils/insertCustomItems.js";
import { runAttachFunction } from "./attachUtils/getAttachFunction.js";
@@ -59,6 +59,28 @@ export const handleAttach = async (req: any, res: any) =>
customEnts: customEnts || [],
});
try {
req.logger.info(`Attach params: `, {
data: {
products: attachParams.products.map((p) => ({
id: p.id,
name: p.name,
processor: p.processor,
version: p.version,
})),
prices: attachParams.prices.map((p) => ({
id: p.id,
config: p.config,
})),
entitlements: attachParams.entitlements.map((e) => ({
internal_feature_id: e.internal_feature_id,
feature_id: e.feature_id,
})),
freeTrial: attachParams.freeTrial,
},
});
} catch (error) {}
await runAttachFunction({
req,
res,

View File

@@ -10,15 +10,10 @@ import {
getNextStartOfMonthUnix,
} from "@/internal/products/prices/billingIntervalUtils.js";
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
import { getLastInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import { getSmallestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { getMergeCusProduct } from "../attachFunctions/addProductFlow/getMergeCusProduct.js";
import {
formatUnixToDate,
formatUnixToDateTime,
notNullish,
nullish,
} from "@/utils/genUtils.js";
import { notNullish, nullish } from "@/utils/genUtils.js";
export const getNewProductPreview = async ({
branch,
@@ -38,7 +33,10 @@ export const getNewProductPreview = async ({
let anchorToUnix = undefined;
if (org.config.anchor_start_of_month) {
anchorToUnix = getNextStartOfMonthUnix(BillingInterval.Month);
anchorToUnix = getNextStartOfMonthUnix({
interval: BillingInterval.Month,
intervalCount: 1,
});
}
const { mergeCusProduct, mergeSubs } = await getMergeCusProduct({
@@ -77,12 +75,16 @@ export const getNewProductPreview = async ({
config,
});
let minInterval = getLastInterval({
// let minInterval = getLastInterval({
// prices: newProduct.prices,
// ents: newProduct.entitlements,
// });
let min = getSmallestInterval({
prices: newProduct.prices,
ents: newProduct.entitlements,
});
let getAligned = notNullish(anchorToUnix) && notNullish(minInterval);
let getAligned = notNullish(anchorToUnix) && notNullish(min);
let dueAt = freeTrial
? freeTrialToStripeTimestamp({
@@ -92,11 +94,16 @@ export const getNewProductPreview = async ({
: getAligned
? getAlignedIntervalUnix({
alignWithUnix: anchorToUnix!,
interval: minInterval,
interval: min!.interval,
intervalCount: min!.intervalCount,
now: attachParams.now,
})
: notNullish(minInterval)
? addBillingIntervalUnix(attachParams.now || Date.now(), minInterval)
: notNullish(min)
? addBillingIntervalUnix({
unixTimestamp: attachParams.now || Date.now(),
interval: min!.interval,
intervalCount: min!.intervalCount,
})
: undefined;
dueNextCycle = !nullish(dueAt)
@@ -125,18 +132,25 @@ export const getNewProductPreview = async ({
// Next cycle at
if (!dueNextCycle) {
if (!isFreeProduct(newProduct.prices) && branch != AttachBranch.OneOff) {
let minInterval = getLastInterval({ prices: newProduct.prices });
let min = getSmallestInterval({
prices: newProduct.prices,
ents: newProduct.entitlements,
});
dueNextCycle = {
line_items: items.filter((item) => {
let price = newProduct.prices.find(
(price) => price.id == item.price_id
);
return price?.config.interval == minInterval;
return (
price?.config.interval == min!.interval &&
(price?.config.interval_count || 1) == (min!.intervalCount || 1)
);
}),
due_at: addBillingIntervalUnix({
unixTimestamp: attachParams.now || Date.now(),
interval: min!.interval,
intervalCount: min!.intervalCount,
}),
due_at: addBillingIntervalUnix(
attachParams.now || Date.now(),
minInterval
),
};
}
}

View File

@@ -1,220 +0,0 @@
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { getOptions } from "@/internal/api/entitled/checkUtils.js";
import { getItemsForCurProduct } from "@/internal/invoices/previewItemUtils/getItemsForCurProduct.js";
import { getItemsForNewProduct } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
import {
addBillingIntervalUnix,
getAlignedIntervalUnix,
} from "@/internal/products/prices/billingIntervalUtils.js";
import { getLastInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { mapToProductItems } from "@/internal/products/productV2Utils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
AttachBranch,
BillingInterval,
FreeTrial,
PreviewLineItem,
Price,
UsageModel,
} from "@autumn/shared";
import Stripe from "stripe";
import {
attachParamToCusProducts,
attachParamsToProduct,
} from "../attachUtils/convertAttachParams.js";
import { intervalsAreSame } from "../attachUtils/getAttachConfig.js";
import { AttachParams } from "../../cusProducts/AttachParams.js";
import { Decimal } from "decimal.js";
const getNextCycleAt = ({
prices,
stripeSubs,
willCycleReset,
interval,
now,
freeTrial,
}: {
prices: Price[];
stripeSubs: Stripe.Subscription[];
willCycleReset: boolean;
interval: BillingInterval;
now?: number;
freeTrial?: FreeTrial | null;
}) => {
now = now || Date.now();
if (freeTrial) {
return {
next_cycle_at:
freeTrialToStripeTimestamp({
freeTrial,
now,
})! * 1000,
};
}
if (willCycleReset) {
const minInterval = getLastInterval({ prices });
return {
next_cycle_at: addBillingIntervalUnix(now, minInterval),
};
}
const minInterval = getLastInterval({ prices });
const nextCycleAt = getAlignedIntervalUnix({
alignWithUnix: stripeSubs[0].current_period_end * 1000,
interval: minInterval,
alwaysReturn: true,
});
return {
next_cycle_at: nextCycleAt,
};
};
export const getUpdateQuantityPreview = async ({
req,
attachParams,
branch,
now,
}: {
req: ExtendedRequest;
attachParams: AttachParams;
branch: AttachBranch;
now: number;
}) => {
const { logtail: logger } = req;
const { stripeCli } = attachParams;
const { curMainProduct, curSameProduct } = attachParamToCusProducts({
attachParams,
});
const curCusProduct = curMainProduct!;
const stripeSubs = await getStripeSubs({
stripeCli,
subIds: curCusProduct?.subscription_ids || [],
expand: ["items.data.price.tiers"],
});
const curPreviewItems = await getItemsForCurProduct({
stripeSubs,
attachParams,
now,
logger,
});
// Get prorated amounts for new product
const newProduct = attachParamsToProduct({ attachParams });
const intervalsSame = intervalsAreSame({ attachParams });
const anchorToUnix =
intervalsSame && stripeSubs.length > 0
? stripeSubs[0].current_period_end * 1000
: undefined;
const newPreviewItems = await getItemsForNewProduct({
newProduct,
attachParams,
now,
anchorToUnix,
freeTrial: attachParams.freeTrial,
stripeSubs,
logger,
});
const lastInterval = getLastInterval({ prices: newProduct.prices });
let dueNextCycle = undefined;
if (!isFreeProduct(newProduct.prices)) {
const nextCycleAt = getNextCycleAt({
prices: newProduct.prices,
stripeSubs,
willCycleReset: !intervalsSame,
interval: lastInterval,
now,
freeTrial: attachParams.freeTrial,
});
let nextCycleItems = await getItemsForNewProduct({
newProduct,
attachParams,
interval: attachParams.freeTrial ? undefined : lastInterval,
logger,
});
dueNextCycle = {
line_items: nextCycleItems,
due_at: nextCycleAt.next_cycle_at,
};
}
let items = [...curPreviewItems, ...newPreviewItems];
for (const item of structuredClone(curPreviewItems)) {
let priceId = item.price_id;
let newItem = newPreviewItems.find((i) => i.price_id == priceId);
if (!newItem) {
continue;
}
let newItemAmount = new Decimal(newItem?.amount ?? 0).toDecimalPlaces(2);
let curItemAmount = new Decimal(item.amount ?? 0).toDecimalPlaces(2);
if (newItemAmount.add(curItemAmount).eq(0)) {
items = items.filter((i) => i.price_id !== priceId);
}
}
const dueTodayAmt = items
.reduce((acc, item) => acc.plus(item.amount ?? 0), new Decimal(0))
.toDecimalPlaces(2)
.toNumber();
let options = getOptions({
prodItems: mapToProductItems({
prices: newProduct.prices,
entitlements: newProduct.entitlements,
features: attachParams.features,
}),
features: attachParams.features,
anchorToUnix,
now,
freeTrial: attachParams.freeTrial,
cusProduct: curSameProduct,
});
items = items.filter((item) => item.amount !== 0);
if (branch == AttachBranch.UpdatePrepaidQuantity) {
items = items.filter((item) => item.usage_model == UsageModel.Prepaid);
dueNextCycle!.line_items = dueNextCycle!.line_items.filter(
(item) => item.usage_model == UsageModel.Prepaid,
);
}
let dueToday:
| {
line_items: PreviewLineItem[];
total: number;
}
| undefined = {
line_items: items,
total: dueTodayAmt,
};
if (branch == AttachBranch.SameCustomEnts) {
dueToday = undefined;
}
return {
currency: attachParams.org.default_currency,
due_today: dueToday,
due_next_cycle: dueNextCycle,
options,
};
};

View File

@@ -5,7 +5,7 @@ import {
} from "../attachUtils/convertAttachParams.js";
import { getStripeSubs } from "@/external/stripe/stripeSubUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { getFirstInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { getLargestInterval } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
import { getItemsForNewProduct } from "@/internal/invoices/previewItemUtils/getItemsForNewProduct.js";
import { getItemsForCurProduct } from "@/internal/invoices/previewItemUtils/getItemsForCurProduct.js";
import { getOptions } from "@/internal/api/entitled/checkUtils.js";
@@ -29,13 +29,12 @@ import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/free
import { Decimal } from "decimal.js";
import { intervalsAreSame } from "../attachUtils/getAttachConfig.js";
import { isFreeProduct } from "@/internal/products/productUtils.js";
import { formatUnixToDateTime, notNullish } from "@/utils/genUtils.js";
import { formatUnixToDateTime, notNullish, nullish } from "@/utils/genUtils.js";
const getNextCycleAt = ({
prices,
stripeSubs,
willCycleReset,
interval,
now,
freeTrial,
branch,
@@ -44,7 +43,6 @@ const getNextCycleAt = ({
prices: Price[];
stripeSubs: Stripe.Subscription[];
willCycleReset: boolean;
interval: BillingInterval;
now?: number;
freeTrial?: FreeTrial | null;
branch: AttachBranch;
@@ -68,17 +66,28 @@ const getNextCycleAt = ({
};
}
if (willCycleReset) {
const firstInterval = getFirstInterval({ prices });
const firstInterval = getLargestInterval({ prices });
if (nullish(firstInterval)) {
return {
next_cycle_at: addBillingIntervalUnix(now, firstInterval),
next_cycle_at: now,
};
}
if (willCycleReset) {
return {
next_cycle_at: addBillingIntervalUnix({
unixTimestamp: now,
interval: firstInterval!.interval,
intervalCount: firstInterval!.intervalCount,
}),
};
}
const firstInterval = getFirstInterval({ prices });
const nextCycleAt = getAlignedIntervalUnix({
alignWithUnix: stripeSubs[0].current_period_end * 1000,
interval: firstInterval,
interval: firstInterval!.interval,
intervalCount: firstInterval!.intervalCount,
alwaysReturn: true,
now,
});
@@ -154,7 +163,7 @@ export const getUpgradeProductPreview = async ({
config,
});
const lastInterval = getFirstInterval({ prices: newProduct.prices });
const largestInterval = getLargestInterval({ prices: newProduct.prices });
let dueNextCycle = undefined;
if (!isFreeProduct(newProduct.prices)) {
@@ -162,7 +171,6 @@ export const getUpgradeProductPreview = async ({
prices: newProduct.prices,
stripeSubs,
willCycleReset: !intervalsSame,
interval: lastInterval,
now,
freeTrial: attachParams.freeTrial,
branch,
@@ -172,7 +180,10 @@ export const getUpgradeProductPreview = async ({
let nextCycleItems = await getItemsForNewProduct({
newProduct,
attachParams,
interval: attachParams.freeTrial ? undefined : lastInterval,
interval: attachParams.freeTrial ? undefined : largestInterval?.interval,
intervalCount: attachParams.freeTrial
? undefined
: largestInterval?.intervalCount,
logger,
withPrepaid,
branch,

View File

@@ -1,410 +0,0 @@
import { Stripe } from "stripe";
import { AttachParams } from "../cusProducts/AttachParams.js";
import { FullCusProduct, InvoiceItem } from "@autumn/shared";
import { BillingInterval, BillingType, UsagePriceConfig } from "@autumn/shared";
import {
getBillingType,
getPriceEntitlement,
getPriceForOverage,
} from "@/internal/products/prices/priceUtils.js";
import {
createStripeCli,
subToAutumnInterval,
} from "@/external/stripe/utils.js";
import { CusEntService } from "../cusProducts/cusEnts/CusEntitlementService.js";
import { payForInvoice } from "@/external/stripe/stripeInvoiceUtils.js";
import { getResetBalancesUpdate } from "../cusProducts/cusEnts/groupByUtils.js";
import {
getCusPriceUsage,
getRelatedCusEnt,
} from "../cusProducts/cusPrices/cusPriceUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { insertInvoiceFromAttach } from "@/internal/invoices/invoiceUtils.js";
import { getStripeNow } from "@/utils/scriptUtils/testClockUtils.js";
import {
cusProductsToCusPrices,
cusProductToEnts,
} from "../cusProducts/cusProductUtils/convertCusProduct.js";
import { getUsageBasedSub } from "@/external/stripe/stripeSubUtils.js";
// Add usage to end of cycle
const addUsageToNextInvoice = async ({
db,
intervalToInvoiceItems,
intervalToSub,
customer,
org,
logger,
attachParams,
}: {
db: DrizzleCli;
intervalToInvoiceItems: any;
intervalToSub: any;
customer: any;
org: any;
logger: any;
attachParams: AttachParams;
}) => {
for (const interval in intervalToInvoiceItems) {
const itemsToInvoice = intervalToInvoiceItems[interval];
if (itemsToInvoice.length === 0) {
continue;
}
// Add items to invoice
const stripeCli = createStripeCli({
org: org,
env: customer.env,
});
for (const item of itemsToInvoice) {
const { amount, description } = item;
logger.info(
` feature: ${item.feature.id}, overage: ${item.overage}, amount: ${amount}`,
);
let relatedSub = intervalToSub[interval];
if (!relatedSub) {
continue;
}
// Create invoice item
let invoiceItem = {
customer: customer.processor.id,
currency: org.default_currency,
description,
price_data: {
product: (item.price.config! as UsagePriceConfig).stripe_product_id!,
unit_amount: Math.round(amount * 100),
currency: org.default_currency,
},
subscription: relatedSub.id,
period: {
start: item.periodStart,
end: item.periodEnd,
},
};
await stripeCli.invoiceItems.create(invoiceItem);
// Update cus ent to 0
await CusEntService.update({
db,
id: item.relatedCusEnt!.id,
updates: getResetBalancesUpdate({
cusEnt: item.relatedCusEnt!,
allowance: 0,
}),
});
// Update existing cusEnt in attachParams
let cusProducts = attachParams.cusProducts;
for (const cusProduct of cusProducts!) {
for (let i = 0; i < cusProduct.customer_entitlements.length; i++) {
let cusEnt = cusProduct.customer_entitlements[i];
if (cusEnt.id === item.relatedCusEnt!.id) {
let balancesUpdate = getResetBalancesUpdate({
cusEnt,
allowance: 0,
});
cusProduct.customer_entitlements[i] = {
...cusEnt,
...balancesUpdate,
};
}
}
}
}
}
};
const invoiceForUsageImmediately = async ({
db,
intervalToInvoiceItems,
customer,
org,
logger,
curCusProduct,
attachParams,
newSubs,
}: {
db: DrizzleCli;
intervalToInvoiceItems: any;
customer: any;
org: any;
logger: any;
curCusProduct: FullCusProduct;
attachParams: AttachParams;
newSubs: Stripe.Subscription[];
}) => {
// 1. Create invoice
const stripeCli = createStripeCli({
org: org,
env: customer.env,
});
const product = curCusProduct.product;
let invoiceItems = Object.values(intervalToInvoiceItems).flat() as any[];
if (invoiceItems.length === 0) {
return;
}
let invoice: Stripe.Invoice;
let newInvoice = false;
if (attachParams.invoiceOnly && newSubs.length > 0) {
invoice = await stripeCli.invoices.retrieve(
newSubs[0].latest_invoice as string,
);
if (invoice.status !== "draft") {
newInvoice = true;
invoice = await stripeCli.invoices.create({
customer: customer.processor.id,
auto_advance: true,
});
}
} else {
newInvoice = true;
invoice = await stripeCli.invoices.create({
customer: customer.processor.id,
auto_advance: true,
});
}
let autumnInvoiceItems: InvoiceItem[] = [];
for (const item of invoiceItems) {
// const amount = getPriceForOverage(item.price, item.overage);
const { amount, description } = item;
let config = item.price.config! as UsagePriceConfig;
// let stripePrice = await stripeCli.prices.retrieve(config.stripe_price_id!);
let stripeProdId = config.stripe_product_id;
if (!stripeProdId) {
try {
let stripePrice = await stripeCli.prices.retrieve(
config.stripe_price_id!,
);
stripeProdId = stripePrice.product as string;
} catch (error) {}
}
if (!stripeProdId) {
stripeProdId = product.processor?.id;
}
logger.info(
`🌟🌟🌟 (Bill remaining) created invoice item: ${description} -- ${amount}`,
);
let invoiceItem = {
customer: customer.processor.id,
invoice: invoice.id,
currency: org.default_currency,
description,
price_data: {
product: stripeProdId!,
unit_amount: Math.round(amount * 100),
currency: org.default_currency,
},
period: {
start: item.periodStart,
end: item.periodEnd,
},
};
let stripeInvoiceItem = await stripeCli.invoiceItems.create(invoiceItem);
autumnInvoiceItems.push({
price_id: item.price.id!,
internal_feature_id: item.feature.internal_id || null,
description: description,
period_start: item.periodStart * 1000,
period_end: item.periodEnd * 1000,
stripe_id: stripeInvoiceItem.id,
});
await CusEntService.update({
db,
id: item.relatedCusEnt!.id,
updates: {
balance: 0,
},
});
let index = curCusProduct.customer_entitlements.findIndex(
(ce) => ce.id === item.relatedCusEnt!.id,
);
curCusProduct.customer_entitlements[index] = {
...curCusProduct.customer_entitlements[index],
balance: 0,
};
}
if (newInvoice) {
await stripeCli.invoices.finalizeInvoice(invoice.id);
const { paid, error } = await payForInvoice({
stripeCli,
paymentMethod: null,
invoiceId: invoice.id,
logger,
});
if (!paid) {
logger.warn("Failed to pay invoice for remaining usages", {
stripeInvoice: newInvoice,
paymentError: error,
});
}
}
await insertInvoiceFromAttach({
db,
attachParams,
invoiceId: invoice.id,
logger,
});
};
const getRemainingUsagesPreview = async ({
intervalToInvoiceItems,
curCusProduct,
}: {
intervalToInvoiceItems: any;
curCusProduct: FullCusProduct;
}) => {
let invoiceItems = Object.values(intervalToInvoiceItems).flat() as any[];
if (invoiceItems.length === 0) {
return;
}
let items = [];
for (const item of invoiceItems) {
const amount = getPriceForOverage(item.price, item.overage);
const description = `${curCusProduct.product.name} - ${
item.feature.name
} x ${Math.round(item.usage)}`;
items.push({
amount,
description,
});
}
return items;
};
// export const billForRemainingUsages = async ({
// db,
// logger,
// attachParams,
// curCusProduct,
// newSubs,
// shouldPreview = false,
// billImmediately = false,
// }: {
// db: DrizzleCli;
// logger: any;
// attachParams: AttachParams;
// curCusProduct: FullCusProduct;
// newSubs: Stripe.Subscription[];
// shouldPreview?: boolean;
// billImmediately?: boolean;
// }) => {
// const { customer_prices, customer_entitlements } = curCusProduct;
// const { customer, org } = attachParams;
// const intervalToSub: any = {};
// for (const sub of newSubs) {
// const interval = subToAutumnInterval(sub);
// if (interval) {
// intervalToSub[interval] = sub;
// }
// }
// const intervalToInvoiceItems: any = {};
// const stripeCli = createStripeCli({
// org: org,
// env: customer.env,
// });
// for (const cp of customer_prices) {
// const config = cp.price.config! as UsagePriceConfig;
// const relatedCusEnt = getRelatedCusEnt({
// cusPrice: cp,
// cusEnts: customer_entitlements,
// });
// const billingType = getBillingType(config);
// if (billingType !== BillingType.UsageInArrear) continue;
// const { usage, overage, description, amount } = getCusPriceUsage({
// cusPrice: cp,
// cusProduct: curCusProduct,
// logger,
// });
// if (overage <= 0) continue; // no overage, no need to bill...
// let interval = config.interval as BillingInterval;
// if (!intervalToInvoiceItems[interval]) {
// intervalToInvoiceItems[interval] = [];
// }
// let sub = intervalToSub[interval];
// const stripeNow = await getStripeNow({
// stripeCli,
// stripeSub: sub,
// });
// intervalToInvoiceItems[interval].push({
// overage,
// usage,
// description,
// amount,
// feature: relatedCusEnt?.entitlement.feature,
// price: cp.price,
// relatedCusEnt,
// periodStart: sub?.current_period_start,
// periodEnd: stripeNow,
// });
// }
// if (shouldPreview) {
// return getRemainingUsagesPreview({
// intervalToInvoiceItems,
// curCusProduct,
// });
// }
// if (billImmediately) {
// await invoiceForUsageImmediately({
// db,
// intervalToInvoiceItems,
// customer,
// org,
// logger,
// curCusProduct,
// attachParams,
// newSubs,
// });
// } else {
// await addUsageToNextInvoice({
// db,
// intervalToInvoiceItems,
// intervalToSub,
// customer,
// org,
// logger,
// attachParams,
// });
// }
// };

View File

@@ -1,4 +1,9 @@
import { getSubItemsForCusProduct } from "@/external/stripe/stripeSubUtils.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
import {
priceToIntervalKey,
toIntervalKey,
} from "@/internal/products/prices/priceUtils/convertPrice.js";
import { notNullish } from "@/utils/genUtils.js";
import { FullCusProduct } from "@autumn/shared";
import { SupabaseClient } from "@supabase/supabase-js";
@@ -32,8 +37,10 @@ export const cancelCurSubs = async ({
cusProduct: curCusProduct,
});
let interval = sub.items.data[0].price.recurring!.interval;
intervalToOtherSubs[interval] = {
// let interval = sub.items.data[0].price.recurring!.interval;
let subInterval = subToAutumnInterval(sub);
let intervalKey = toIntervalKey(subInterval);
intervalToOtherSubs[intervalKey] = {
otherSubItems,
otherSub: sub,
};

View File

@@ -1,569 +0,0 @@
// import { getStripeExpandedInvoice } from "@/external/stripe/stripeInvoiceUtils.js";
// import {
// getStripeSchedules,
// getStripeSubs,
// } from "@/external/stripe/stripeSubUtils.js";
// import { createStripeCli } from "@/external/stripe/utils.js";
// import { freeTrialToStripeTimestamp } from "@/internal/products/free-trials/freeTrialUtils.js";
// import {
// attachToInsertParams,
// isFreeProduct,
// } from "@/internal/products/productUtils.js";
// import RecaseError from "@/utils/errorUtils.js";
// import {
// FullCusProduct,
// ErrCode,
// FullProduct,
// CusProductStatus,
// APIVersion,
// UsagePriceConfig,
// AttachScenario,
// } from "@autumn/shared";
// import { StatusCodes } from "http-status-codes";
// import Stripe from "stripe";
// import { createFullCusProduct } from "../add-product/createFullCusProduct.js";
// import { handleAddProduct } from "../attach/attachFunctions/addProductFlow/handleAddProduct.js";
// import {
// AttachParams,
// AttachResultSchema,
// } from "../cusProducts/AttachParams.js";
// import { CusProductService } from "../cusProducts/CusProductService.js";
// import {
// getInvoiceItems,
// insertInvoiceFromAttach,
// } from "@/internal/invoices/invoiceUtils.js";
// import { InvoiceService } from "@/internal/invoices/InvoiceService.js";
// import { updateScheduledSubWithNewItems } from "./scheduleUtils/updateScheduleWithNewItems.js";
// // import { billForRemainingUsages } from "./billRemainingUsages.js";
// import { updateStripeSubscription } from "@/external/stripe/stripeSubUtils/updateStripeSub.js";
// import { createStripeSub } from "@/external/stripe/stripeSubUtils/createStripeSub.js";
// import {
// addBillingIntervalUnix,
// subtractBillingIntervalUnix,
// } from "@/internal/products/prices/billingIntervalUtils.js";
// import { differenceInSeconds } from "date-fns";
// import { SuccessCode } from "@autumn/shared";
// import { notNullish } from "@/utils/genUtils.js";
// import { DrizzleCli } from "@/db/initDrizzle.js";
// import { ExtendedRequest } from "@/utils/models/Request.js";
// import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
// export enum ProrationBehavior {
// Immediately = "immediately",
// NextBilling = "next_billing",
// None = "none",
// }
// // UPGRADE FUNCTIONS
// export const handleStripeSubUpdate = async ({
// db,
// stripeCli,
// curCusProduct,
// attachParams,
// disableFreeTrial,
// stripeSubs,
// logger,
// carryExistingUsages = false,
// prorationBehavior = ProrationBehavior.Immediately,
// shouldPreview = false,
// }: {
// db: DrizzleCli;
// stripeCli: Stripe;
// curCusProduct: FullCusProduct;
// attachParams: AttachParams;
// disableFreeTrial?: boolean;
// stripeSubs: Stripe.Subscription[];
// logger: any;
// carryExistingUsages?: boolean;
// prorationBehavior?: ProrationBehavior;
// shouldPreview?: boolean;
// }) => {
// // HANLDE UPGRADE
// // 1. Get item sets
// const itemSets = await getStripeSubItems({
// attachParams,
// carryExistingUsages,
// });
// const firstSub = stripeSubs[0];
// const firstItemSet = itemSets[0];
// let curPrices = curCusProduct.customer_prices.map((cp) => cp.price);
// // 1. DELETE ITEMS FROM CURRENT SUB THAT CORRESPOND TO OLD PRODUCT
// for (const item of firstSub.items.data) {
// let stripePriceExists = curPrices.some(
// (p) =>
// p.config!.stripe_price_id === item.price.id ||
// (p.config as UsagePriceConfig).stripe_product_id === item.price.product,
// );
// let stripeProdExists =
// item.price.product == curCusProduct.product.processor?.id;
// if (!stripePriceExists && !stripeProdExists) {
// continue;
// }
// firstItemSet.items.push({
// id: item.id,
// deleted: true,
// });
// }
// // 2. Add trial to new subscription?
// let trialEnd;
// if (!disableFreeTrial) {
// trialEnd = freeTrialToStripeTimestamp({
// freeTrial: attachParams.freeTrial,
// now: attachParams.now,
// });
// }
// // 3. Update current subscription
// let newSubs = [];
// const subUpdateRes = await updateStripeSubscription({
// db,
// stripeCli,
// subscriptionId: firstSub.id,
// trialEnd,
// org: attachParams.org,
// customer: attachParams.customer,
// invoiceOnly: attachParams.invoiceOnly || false,
// prorationBehavior,
// logger,
// itemSet: firstItemSet,
// shouldPreview,
// });
// if (shouldPreview) {
// return subUpdateRes;
// }
// let subUpdate = subUpdateRes as Stripe.Subscription;
// newSubs.push(subUpdate);
// // 4. If scheduled_ids exist, need to update schedule too (BRUH)!
// if (curCusProduct.scheduled_ids && curCusProduct.scheduled_ids.length > 0) {
// let schedules = await getStripeSchedules({
// stripeCli,
// scheduleIds: curCusProduct.scheduled_ids,
// });
// for (const scheduleObj of schedules) {
// const { interval, schedule } = scheduleObj;
// // If schedule has passed, skip this step.
// let phase = schedule.phases.length > 0 ? schedule.phases[0] : null;
// let now = Date.now();
// if (schedule.test_clock) {
// let testClock = await stripeCli.testHelpers.testClocks.retrieve(
// schedule.test_clock as string,
// );
// now = testClock.frozen_time * 1000;
// }
// if (phase && phase.start_date * 1000 < now) {
// logger.info("Note: Schedule has passed, skipping");
// continue;
// }
// // Get corresponding item set
// const itemSet = itemSets.find((itemSet) => itemSet.interval === interval);
// if (!itemSet) {
// continue;
// }
// await updateScheduledSubWithNewItems({
// scheduleObj,
// newItems: itemSet.items,
// stripeCli,
// cusProductsForGroup: [curCusProduct],
// itemSet,
// db,
// org: attachParams.org,
// env: attachParams.customer.env,
// });
// }
// }
// // 5. Insert invoice
// await insertInvoiceFromAttach({
// db,
// attachParams,
// invoiceId: subUpdate.latest_invoice as string,
// logger,
// });
// // 2. Create new subscriptions
// let newSubIds = [];
// newSubIds.push(firstSub.id);
// const newItemSets = itemSets.slice(1);
// let invoiceIds = [];
// // CREATE NEW SUBSCRIPTIONS
// for (const itemSet of newItemSets) {
// // 1. Next billing date for first sub
// // const nextCycleAnchor = firstSub.current_period_end * 1000;
// const nextCycleAnchor = subUpdate.current_period_end * 1000;
// let nextCycleAnchorUnix = nextCycleAnchor;
// const naturalBillingDate = addBillingIntervalUnix(
// Date.now(),
// itemSet.interval,
// );
// while (true) {
// const subtractedUnix = subtractBillingIntervalUnix(
// nextCycleAnchorUnix,
// itemSet.interval,
// );
// if (subtractedUnix < Date.now()) {
// break;
// }
// nextCycleAnchorUnix = subtractedUnix;
// }
// let billingCycleAnchorUnix: number | undefined = nextCycleAnchorUnix;
// if (
// differenceInSeconds(
// new Date(naturalBillingDate),
// new Date(nextCycleAnchorUnix),
// ) < 60
// ) {
// billingCycleAnchorUnix = undefined;
// }
// const newSub = (await createStripeSub({
// db,
// stripeCli,
// customer: attachParams.customer,
// org: attachParams.org,
// itemSet,
// invoiceOnly: attachParams.invoiceOnly || false,
// freeTrial: attachParams.freeTrial,
// anchorToUnix: billingCycleAnchorUnix,
// })) as Stripe.Subscription;
// newSubs.push(newSub);
// newSubIds.push(newSub.id);
// invoiceIds.push(newSub.latest_invoice as string);
// }
// // 3. Cancel old subscriptions
// let remainingExistingSubIds = stripeSubs.slice(1).map((sub) => sub.id);
// return {
// subUpdate,
// newSubIds,
// invoiceIds,
// remainingExistingSubIds,
// newSubs,
// };
// };
// const handleOnlyEntsChanged = async ({
// req,
// res,
// attachParams,
// curCusProduct,
// carryExistingUsages = false,
// }: {
// req: any;
// res: any;
// attachParams: AttachParams;
// curCusProduct: FullCusProduct;
// carryExistingUsages?: boolean;
// }) => {
// const logger = req.logtail;
// logger.info("Only entitlements changed, no need to update prices");
// // Remove subscription from previous cus product
// await CusProductService.update({
// db: req.db,
// cusProductId: curCusProduct.id,
// updates: {
// subscription_ids: [],
// },
// });
// await createFullCusProduct({
// db: req.db,
// attachParams: attachToInsertParams(attachParams, attachParams.products[0]),
// subscriptionIds: curCusProduct.subscription_ids || [],
// disableFreeTrial: false,
// keepResetIntervals: true,
// carryExistingUsages,
// logger,
// });
// logger.info("✅ Successfully updated entitlements for product");
// let org = attachParams.org;
// let apiVersion = org.api_version || APIVersion.v1;
// if (apiVersion >= APIVersion.v1_1) {
// res.status(200).json(
// AttachResultSchema.parse({
// customer_id: attachParams.customer.id,
// product_ids: attachParams.products.map((p) => p.id),
// code: SuccessCode.FeaturesUpdated,
// message: `Successfully updated features for customer ${attachParams.customer.id} on product ${attachParams.products[0].name}`,
// }),
// );
// } else {
// res.status(200).json({
// success: true,
// message: `Successfully updated entitlements for ${curCusProduct.product.name}`,
// });
// }
// };
// export const handleUpgrade = async ({
// req,
// res,
// attachParams,
// curCusProduct,
// curFullProduct,
// hasPricesChanged = true,
// fromReq = true,
// carryExistingUsages = false,
// prorationBehavior,
// newVersion = false,
// updateSameProduct = false,
// }: {
// req: ExtendedRequest;
// res: any;
// attachParams: AttachParams;
// curCusProduct: FullCusProduct;
// curFullProduct: FullProduct;
// hasPricesChanged?: boolean;
// fromReq?: boolean;
// carryExistingUsages?: boolean;
// prorationBehavior?: ProrationBehavior;
// newVersion?: boolean;
// updateSameProduct?: boolean;
// }) => {
// const logger = req.logtail;
// const { org, customer, products } = attachParams;
// let product = products[0];
// let disableFreeTrial = false;
// if (newVersion) {
// disableFreeTrial = true;
// }
// if (!hasPricesChanged) {
// await handleOnlyEntsChanged({
// req,
// res,
// attachParams,
// curCusProduct,
// carryExistingUsages,
// });
// return;
// }
// logger.info(
// `Upgrading ${curFullProduct.name} to ${product.name} for ${customer.id}`,
// );
// const stripeCli = createStripeCli({ org, env: customer.env });
// const stripeSubs = await getStripeSubs({
// stripeCli,
// subIds: curCusProduct.subscription_ids,
// });
// // 1. If current product has trial and new product has trial, cancel and start new subscription
// let trialToTrial =
// curCusProduct.trial_ends_at &&
// curCusProduct.trial_ends_at > Date.now() &&
// attachParams.freeTrial &&
// !disableFreeTrial;
// let trialToPaid =
// curCusProduct.trial_ends_at &&
// curCusProduct.trial_ends_at > Date.now() &&
// !attachParams.freeTrial &&
// !newVersion; // If trial to paid and not migrating, cancel trial and start new sub immediately.
// // 2. If upgrade is free to paid, or paid to free (migration / update)
// let toFreeProduct = isFreeProduct(attachParams.prices);
// let paidToFreeProduct =
// isFreeProduct(curCusProduct.customer_prices.map((cp) => cp.price)) &&
// !isFreeProduct(attachParams.prices);
// if (trialToTrial || trialToPaid || toFreeProduct || paidToFreeProduct) {
// if (trialToTrial) {
// logger.info(
// `Upgrading from trial to trial, cancelling and starting new subscription`,
// );
// } else if (toFreeProduct) {
// logger.info(
// `switching to free product, cancelling (if needed) and adding free product`,
// );
// }
// await handleAddProduct({
// req,
// res,
// attachParams,
// fromRequest: fromReq,
// carryExistingUsages,
// keepResetIntervals: newVersion, // keep reset intervals if upgrading version (migrations)
// disableMerge: true,
// });
// if (notNullish(curCusProduct.subscription_ids)) {
// for (const subId of curCusProduct.subscription_ids!) {
// try {
// await stripeCli.subscriptions.cancel(subId);
// } catch (error) {
// throw new RecaseError({
// message: `Handling upgrade (cur product on trial): failed to cancel subscription ${subId}`,
// code: ErrCode.StripeCancelSubscriptionFailed,
// statusCode: StatusCodes.BAD_REQUEST,
// data: error,
// });
// }
// }
// }
// return;
// }
// logger.info("1. Updating current subscription to new product");
// let {
// subUpdate,
// newSubIds,
// invoiceIds,
// remainingExistingSubIds,
// newSubs,
// }: any = await handleStripeSubUpdate({
// db: req.db,
// curCusProduct,
// stripeCli,
// attachParams,
// disableFreeTrial,
// stripeSubs,
// logger,
// carryExistingUsages,
// prorationBehavior,
// });
// // logger.info("2. Bill for remaining usages");
// // await billForRemainingUsages({
// // db: req.db,
// // attachParams,
// // curCusProduct,
// // newSubs,
// // logger,
// // });
// logger.info(
// "2.1. Remove old subscription ID from old cus product and expire",
// );
// await CusProductService.update({
// db: req.db,
// cusProductId: curCusProduct.id,
// updates: {
// subscription_ids: curCusProduct.subscription_ids!.filter(
// (subId) => subId !== subUpdate.id,
// ),
// processor: {
// ...curCusProduct.processor,
// subscription_id: null,
// } as any,
// status: CusProductStatus.Expired,
// },
// });
// if (remainingExistingSubIds && remainingExistingSubIds.length > 0) {
// logger.info("2.2. Canceling old subscriptions");
// for (const subId of remainingExistingSubIds) {
// logger.info(" - Cancelling old subscription", subId);
// await stripeCli.subscriptions.cancel(subId);
// }
// }
// // Handle backend
// logger.info("3. Creating new full cus product");
// await createFullCusProduct({
// db: req.db,
// attachParams: attachToInsertParams(attachParams, products[0]),
// subscriptionIds: newSubIds,
// anchorToUnix:
// newSubs.length > 0 ? newSubs[0].current_period_end * 1000 : undefined,
// disableFreeTrial,
// carryExistingUsages,
// carryOverTrial: true,
// scenario: AttachScenario.Upgrade,
// logger,
// });
// // Create invoices
// logger.info("4. Creating invoices");
// logger.info(`Invoice IDs: ${invoiceIds}`);
// const batchInsertInvoice = [];
// for (const invoiceId of invoiceIds) {
// const insertInvoice = async () => {
// const stripeInvoice = await getStripeExpandedInvoice({
// stripeCli,
// stripeInvoiceId: invoiceId,
// });
// let autumnInvoiceItems = await getInvoiceItems({
// stripeInvoice,
// prices: attachParams.prices,
// logger,
// });
// await InvoiceService.createInvoiceFromStripe({
// db: req.db,
// stripeInvoice,
// internalCustomerId: customer.internal_id,
// internalEntityId: attachParams.internalEntityId,
// org,
// productIds: products.map((p) => p.id),
// internalProductIds: products.map((p) => p.internal_id),
// items: autumnInvoiceItems,
// });
// };
// batchInsertInvoice.push(insertInvoice());
// }
// await Promise.all(batchInsertInvoice);
// logger.info("✅ Done!");
// if (fromReq) {
// if (org.api_version! >= APIVersion.v1_1) {
// res.status(200).json(
// AttachResultSchema.parse({
// customer_id: customer.id,
// product_ids: products.map((p) => p.id),
// code: updateSameProduct
// ? SuccessCode.UpdatedSameProduct
// : newVersion
// ? SuccessCode.UpgradedToNewVersion
// : SuccessCode.UpgradedToNewProduct,
// message: `Successfully attached ${product.name} to ${customer.name} -- upgraded from ${curFullProduct.name}`,
// }),
// );
// } else {
// res.status(200).json({
// success: true,
// message: `Successfully attached ${product.name} to ${customer.name} -- upgraded from ${curFullProduct.name}`,
// });
// }
// }
// };

View File

@@ -6,6 +6,7 @@ import {
AppEnv,
AttachScenario,
FullCusProduct,
intervalsSame,
Organization,
Product,
} from "@autumn/shared";
@@ -30,6 +31,7 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import { getStripeSubItems } from "@/external/stripe/stripeSubUtils/getStripeSubItems.js";
import { notNullish } from "@/utils/genUtils.js";
import { subToAutumnInterval } from "@/external/stripe/utils.js";
export const getPricesForCusProduct = ({
cusProduct,
@@ -134,7 +136,7 @@ export const cancelFutureProductSchedule = async ({
// Case where cur scheduled product is not free
for (const scheduleObj of schedules) {
const { schedule, interval, prices } = scheduleObj;
const { schedule, interval, intervalCount, prices } = scheduleObj;
if (inIntervals && !inIntervals.includes(interval!)) {
continue;
@@ -142,7 +144,7 @@ export const cancelFutureProductSchedule = async ({
// 1. Remove cur scheduled product items from schedule
const activeCusProducts = cusProducts.filter((cusProduct) =>
isActiveStatus(cusProduct?.status),
isActiveStatus(cusProduct?.status)
);
const filteredScheduleItems = getFilteredScheduleItems({
@@ -152,8 +154,11 @@ export const cancelFutureProductSchedule = async ({
// 2. If any items left, update schedule with cur main product!
if (filteredScheduleItems.length > 0) {
let oldItemSet = oldItemSets.find(
(itemSet) => itemSet.interval === interval,
let oldItemSet = oldItemSets.find((itemSet) =>
intervalsSame({
intervalA: { interval, intervalCount },
intervalB: itemSet,
})
);
await updateScheduledSubWithNewItems({
@@ -186,7 +191,7 @@ export const cancelFutureProductSchedule = async ({
cusProductId: curMainProduct!.id,
updates: {
scheduled_ids: curMainProduct!.scheduled_ids?.filter(
(id) => id !== schedule.id,
(id) => id !== schedule.id
),
},
});
@@ -202,15 +207,21 @@ export const cancelFutureProductSchedule = async ({
await stripeCli.subscriptionSchedules.cancel(schedule.id);
} catch (error: any) {
logger.warn(
`❌ Error cancelling schedule: ${schedule.id}, ${error.message}`,
`❌ Error cancelling schedule: ${schedule.id}, ${error.message}`
);
}
const subWithSameInterval = curSubs.find(
(sub) =>
const subWithSameInterval = curSubs.find((sub) => {
let subInterval = subToAutumnInterval(sub);
// sub.items.data[0]?.price?.recurring?.interval === interval
return (
sub.items.data.length > 0 &&
sub.items.data[0]?.price?.recurring?.interval === interval,
);
intervalsSame({
intervalA: { interval, intervalCount },
intervalB: subInterval,
})
);
});
if (subWithSameInterval && renewCurProduct) {
await stripeCli.subscriptions.update(subWithSameInterval.id, {
@@ -291,7 +302,7 @@ export const cancelFutureProductSchedule = async ({
}
} catch (error) {
logger.error(
`❌ Error sending products updated webhook from cancelFutureProductSchedule: ${error}`,
`❌ Error sending products updated webhook from cancelFutureProductSchedule: ${error}`
);
}
}
@@ -310,7 +321,7 @@ export const cancelFutureProductSchedule = async ({
batchRenew.push(
stripeCli.subscriptions.update(subId, {
cancel_at: null,
}),
})
);
}

View File

@@ -4,5 +4,6 @@ import Stripe from "stripe";
export interface ScheduleObj {
schedule: Stripe.SubscriptionSchedule;
interval: BillingInterval;
intervalCount: number;
prices: Stripe.Price[];
}

View File

@@ -1,4 +1,9 @@
import { AppEnv, FullCusProduct, Organization } from "@autumn/shared";
import {
AppEnv,
FullCusProduct,
intervalsSame,
Organization,
} from "@autumn/shared";
import { getExistingCusProducts } from "../../cusProducts/cusProductUtils/getExistingCusProducts.js";
import { getStripeSchedules } from "@/external/stripe/stripeSubUtils.js";
import { getScheduleIdsFromCusProducts } from "../scheduleUtils.js";
@@ -23,7 +28,7 @@ export const getOtherCusProductsOnSub = async ({
if (
cusProduct.id === curMainProduct.id ||
!curMainSubIds?.some((subId) =>
cusProduct?.subscription_ids?.includes(subId),
cusProduct?.subscription_ids?.includes(subId)
)
) {
continue;
@@ -72,10 +77,13 @@ export const addCurMainProductToSchedule = async ({
});
for (const scheduleObj of schedules) {
const { schedule, interval } = scheduleObj;
const { schedule, interval, intervalCount } = scheduleObj;
let oldItemSet = oldItemSets.find(
(itemSet) => itemSet.interval === interval,
let oldItemSet = oldItemSets.find((itemSet) =>
intervalsSame({
intervalA: { interval, intervalCount },
intervalB: itemSet,
})
);
await updateScheduledSubWithNewItems({
@@ -99,7 +107,7 @@ export const addCurMainProductToSchedule = async ({
});
logger.info(
`✅ Added old items for product ${curMainProduct.product.name} to schedule: ${schedule.id}`,
`✅ Added old items for product ${curMainProduct.product.name} to schedule: ${schedule.id}`
);
}
};

View File

@@ -10,7 +10,7 @@ export const getFilteredScheduleItems = ({
scheduleObj: ScheduleObj;
cusProducts: (FullCusProduct | undefined)[];
}) => {
const { schedule, interval, prices } = scheduleObj;
const { schedule, prices } = scheduleObj;
let scheduleItems = schedule.phases[0].items;
let curPrices: any[] = [];
@@ -31,10 +31,10 @@ export const getFilteredScheduleItems = ({
curPrices.some(
(price) =>
price.config?.stripe_price_id === scheduleItem.price ||
price.config?.stripe_product_id === stripePrice?.product,
price.config?.stripe_product_id === stripePrice?.product
) ||
products.some(
(product) => product.processor?.id === stripePrice?.product,
(product) => product.processor?.id === stripePrice?.product
);
return !inCurProduct;

View File

@@ -70,6 +70,7 @@ export type AttachParams = {
scenario?: AttachScenario;
fromMigration?: boolean;
finalizeInvoice?: boolean;
req?: any;
};
@@ -104,6 +105,7 @@ export type InsertCusProductParams = {
internalEntityId?: string;
fromMigration?: boolean;
apiVersion?: APIVersion;
finalizeInvoice?: boolean;
};
export const AttachResultSchema = z.object({

View File

@@ -5,6 +5,8 @@ import {
CusProductStatus,
Customer,
EntInterval,
entIntervalsDifferent,
entIntervalToValue,
Entitlement,
EntitlementWithFeature,
Entity,
@@ -46,14 +48,14 @@ export const getCusEntMasterBalance = ({
(acc, curr) => {
return acc + curr.balance;
},
0,
0
);
let totalAdjustment = Object.values(cusEnt.entities || {}).reduce(
(acc, curr) => {
return acc + curr.adjustment;
},
0,
0
);
return {
@@ -69,7 +71,7 @@ export const getCusEntMasterBalance = ({
entities &&
entities.filter(
(entity) =>
entity.internal_feature_id == feature.internal_id && entity.deleted,
entity.internal_feature_id == feature.internal_id && entity.deleted
).length;
return {
@@ -120,7 +122,7 @@ export const sortCusEntsForDeduction = (
cusEnts: (FullCustomerEntitlement & {
customer_product?: FullCusProduct;
})[],
reverseOrder: boolean = false,
reverseOrder: boolean = false
) => {
let intervalOrder: Record<EntInterval, number> = {
[EntInterval.Minute]: 0, // 1 minute
@@ -206,11 +208,15 @@ export const sortCusEntsForDeduction = (
}
// 3. Sort by interval
if (aEnt.interval && bEnt.interval && aEnt.interval != bEnt.interval) {
let aVal = entIntervalToValue(aEnt.interval, aEnt.interval_count);
let bVal = entIntervalToValue(bEnt.interval, bEnt.interval_count);
if (aEnt.interval && bEnt.interval && !aVal.eq(bVal)) {
if (reverseOrder) {
return intervalOrder[bEnt.interval] - intervalOrder[aEnt.interval];
return bVal.sub(aVal).toNumber();
// return intervalOrder[bEnt.interval] - intervalOrder[aEnt.interval];
} else {
return intervalOrder[aEnt.interval] - intervalOrder[bEnt.interval];
return aVal.sub(bVal).toNumber();
// return intervalOrder[aEnt.interval] - intervalOrder[bEnt.interval];
}
}
@@ -241,7 +247,7 @@ export const sortCusEntsForDeduction = (
// Get related cusPrice
export const getRelatedCusPrice = (
cusEnt: FullCustomerEntitlement,
cusPrices: FullCustomerPrice[],
cusPrices: FullCustomerPrice[]
) => {
return cusPrices.find((cusPrice) => {
let productMatch =
@@ -328,7 +334,7 @@ export const getResetBalance = ({
return (entitlement.allowance || 0) + quantity! * billingUnits!;
} catch (error) {
console.log(
"WARNING: Failed to return quantity * billing units, returning allowance...",
"WARNING: Failed to return quantity * billing units, returning allowance..."
);
return entitlement.allowance || 0;
}
@@ -347,14 +353,14 @@ export const getUnlimitedAndUsageAllowed = ({
(cusEnt) =>
cusEnt.internal_feature_id === internalFeatureId &&
(cusEnt.entitlement.allowance_type === AllowanceType.Unlimited ||
cusEnt.unlimited),
cusEnt.unlimited)
);
const usageAllowed = cusEnts.some(
(ent) =>
ent.internal_feature_id === internalFeatureId &&
ent.usage_allowed &&
nullish(ent.entitlement.usage_limit),
nullish(ent.entitlement.usage_limit)
);
return { unlimited, usageAllowed };
@@ -426,7 +432,7 @@ export const cusEntsContainFeature = ({
feature: Feature;
}) => {
return cusEnts.some(
(cusEnt) => cusEnt.internal_feature_id === feature.internal_id!,
(cusEnt) => cusEnt.internal_feature_id === feature.internal_id!
);
};
@@ -516,7 +522,7 @@ export const getExistingUsageFromCusProducts = ({
!cp.product.is_add_on &&
(internalEntityId
? cp.internal_entity_id === internalEntityId
: nullish(cp.internal_entity_id)),
: nullish(cp.internal_entity_id))
)
.flatMap((cp) => cp.customer_entitlements)
.find((ce) => ce.internal_feature_id === entitlement.internal_feature_id);
@@ -537,15 +543,15 @@ export const getExistingUsageFromCusProducts = ({
// Get options
let cusProduct = cusProducts?.find(
(cp) => cp.id === existingCusEnt.customer_product_id,
(cp) => cp.id === existingCusEnt.customer_product_id
);
let options = getEntOptions(
cusProduct?.options || [],
existingCusEnt.entitlement,
existingCusEnt.entitlement
);
let price = getRelatedCusPrice(
existingCusEnt,
cusProduct?.customer_prices || [],
cusProduct?.customer_prices || []
);
let existingAllowance = getResetBalance({
entitlement: existingCusEnt.entitlement,

View File

@@ -37,7 +37,7 @@ export const getExistingCusEntAndUsage = async ({
// 1. If there is only one cus ent, return it and usage
let similarCusEnts = curCusProduct.customer_entitlements.filter(
(ce) => ce.internal_feature_id === entitlement.internal_feature_id,
(ce) => ce.internal_feature_id === entitlement.internal_feature_id
// &&
// ce.entitlement.interval === entitlement.interval
);
@@ -47,8 +47,8 @@ export const getExistingCusEntAndUsage = async ({
"Similar entitlements:",
similarCusEnts.map(
(ce) =>
`${ce.entitlement.feature_id} (${ce.entitlement.interval}) (${ce.balance})`,
),
`${ce.entitlement.feature_id} (${ce.entitlement.interval}) (${ce.balance})`
)
);
if (similarCusEnts.length === 1) {
@@ -79,9 +79,9 @@ export const getExistingUsages = ({
// Get entityUsage
for (const entity of entities) {
let feature = features.find(
(f) => f.internal_id === entity.internal_feature_id,
(f) => f.internal_id === entity.internal_feature_id
);
let key = `${feature?.id}-${EntInterval.Lifetime}`;
let key = `${feature?.id}-${EntInterval.Lifetime}-1`;
if (!usages[key]) {
usages[key] = {
@@ -96,7 +96,7 @@ export const getExistingUsages = ({
for (const cusEnt of curCusProduct?.customer_entitlements || []) {
let ent = cusEnt.entitlement;
let key = `${ent.feature_id}-${ent.interval}`;
let key = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`;
let feature = ent.feature;
if (feature.type == FeatureType.Boolean) {
continue;
@@ -199,8 +199,8 @@ export const addExistingUsagesToCusEnts = ({
"Sorted cusEnts:",
fullCusEnts.map(
(ce) =>
`${ce.entitlement.feature_id} (${ce.entitlement.interval}), balance: ${ce.balance}`,
),
`${ce.entitlement.feature_id} (${ce.entitlement.interval}), balance: ${ce.balance}`
)
);
}
@@ -212,7 +212,7 @@ export const addExistingUsagesToCusEnts = ({
for (const cusEnt of fullCusEnts) {
let ent = cusEnt.entitlement;
let cusEntKey = `${ent.feature_id}-${ent.interval}`;
let cusEntKey = `${ent.feature_id}-${ent.interval}-${ent.interval_count || 1}`;
let fromEntities = existingUsages[key].fromEntities;
if (cusEntKey !== key) {

View File

@@ -79,7 +79,7 @@ export const cancelCusProductSubscriptions = async ({
subIds: cusProduct.subscription_ids,
});
latestSubEnd = stripeSubs[0].current_period_end;
latestSubEnd = stripeSubs?.[0]?.current_period_end;
}
const cancelStripeSub = async (subId: string) => {

View File

@@ -62,6 +62,7 @@ export const initNextResetAt = ({
nextResetAtCalculated = getNextEntitlementReset(
nextResetAtCalculated || new UTCDate(now),
resetInterval,
entitlement.interval_count || 1
).getTime();
// If anchorToUnix, align next reset at to anchorToUnix...

View File

@@ -41,7 +41,7 @@ export const createNewCustomer = async ({
const { db, org, env, logger } = req;
logger.info(
`Creating customer: ${customer.email || customer.id}, org: ${org.slug}`,
`Creating customer: ${customer.email || customer.id}, org: ${org.slug}`
);
const defaultProds = await ProductService.listDefault({
@@ -149,7 +149,10 @@ export const createNewCustomer = async ({
}),
nextResetAt,
anchorToUnix: org.config.anchor_start_of_month
? getNextStartOfMonthUnix(BillingInterval.Month)
? getNextStartOfMonthUnix({
interval: BillingInterval.Month,
intervalCount: 1,
})
: undefined,
scenario: AttachScenario.New,
logger,

View File

@@ -87,11 +87,14 @@ export const featuresToObject = ({
next_reset_at: getEarliestNextResetAt(relatedEnts),
interval: relatedEnts.length == 1 ? relatedEnts[0].interval : "multiple",
interval_count:
relatedEnts.length == 1 ? relatedEnts[0].interval_count : null,
overage_allowed: relatedEnts.some((e) => e.overage_allowed),
breakdown:
!unlimited && relatedEnts.length > 1
? relatedEnts.map((e) => ({
interval: e.interval!,
interval_count: e.interval_count,
balance: e.balance,
usage: e.usage,
included_usage: e.included_usage,

View File

@@ -141,7 +141,7 @@ export const getRolloverFields = ({
);
}
} else {
return cusEnt.rollovers.reduce(
return rollovers.reduce(
(acc, rollover) => {
return {
balance: acc.balance + rollover.balance,
@@ -204,7 +204,7 @@ export const getCusBalances = async ({
const cusProduct = cusEnt.customer_product;
const feature = cusEnt.entitlement.feature;
const ent: EntitlementWithFeature = cusEnt.entitlement;
let key = `${ent.interval || "no-interval"}-${feature.id}`;
let key = `${ent.interval || "no-interval"}-${ent.interval_count || 1}-${feature.id}`;
// 1. Handle boolean
let isBoolean = feature.type == FeatureType.Boolean;
@@ -239,6 +239,11 @@ export const getCusBalances = async ({
unlimited: isBoolean ? undefined : unlimited,
interval:
isBoolean || unlimited ? undefined : ent.interval || undefined,
// interval_count:
// isBoolean || unlimited
// ? undefined
// : ent.interval_count || undefined,
balance: isBoolean ? undefined : unlimited ? null : 0,
total: isBoolean || unlimited ? undefined : 0,
adjustment: isBoolean || unlimited ? undefined : 0,
@@ -252,6 +257,7 @@ export const getCusBalances = async ({
isBoolean || unlimited ? undefined : cusEnt.next_reset_at;
data[key].allowance = isBoolean || unlimited ? undefined : 0;
data[key].usage_limit = isBoolean || unlimited ? undefined : 0;
data[key].interval_count = ent.interval_count || 1;
}
}
}

View File

@@ -65,31 +65,6 @@ expireRouter.post("", async (req, res) =>
prorate,
});
// console.log(
// "CUs products to expire",
// cusProductsToExpire.map((c) => c.product.id)
// );
// throw new Error("test");
// if (!cusProductsToExpire) {
// throw new RecaseError({
// code: ErrCode.ProductNotFound,
// message: `Product ${product_id} not found for customer ${customer_id}`,
// });
// }
// // Handle case if there are two products to expire...
// for (const cusProduct of cusProductsToExpire) {
// await expireCusProduct({
// req,
// cusProduct,
// fullCus,
// expireImmediately,
// prorate,
// });
// }
res.status(200).json({
success: true,
customer_id: customer_id,

View File

@@ -2,13 +2,16 @@ import { DrizzleCli } from "@/db/initDrizzle.js";
import { createStripeCli } from "@/external/stripe/utils.js";
import { getExistingCusProducts } from "@/internal/customers/cusProducts/cusProductUtils/getExistingCusProducts.js";
import { cancelFutureProductSchedule } from "@/internal/customers/change-product/scheduleUtils.js";
import { CusProductService } from "@/internal/customers/cusProducts/CusProductService.js";
import {
ACTIVE_STATUSES,
CusProductService,
} from "@/internal/customers/cusProducts/CusProductService.js";
import {
cancelCusProductSubscriptions,
expireAndActivate,
fullCusProductToProduct,
} from "@/internal/customers/cusProducts/cusProductUtils.js";
import { isOneOff } from "@/internal/products/productUtils.js";
import { isFreeProduct, isOneOff } from "@/internal/products/productUtils.js";
import RecaseError, { handleRequestError } from "@/utils/errorUtils.js";
import { ExtendedRequest } from "@/utils/models/Request.js";
import {
@@ -21,6 +24,7 @@ import {
} from "@autumn/shared";
import { StatusCodes } from "http-status-codes";
import { CusService } from "../CusService.js";
import { cusProductToPrices } from "../cusProducts/cusProductUtils/convertCusProduct.js";
export const removeScheduledProduct = async ({
req,
@@ -112,14 +116,29 @@ export const expireCusProduct = async ({
// 1. If main product, can't expire if there's scheduled product
let isMain = !cusProduct.product.is_add_on;
if (isMain) {
let { curScheduledProduct: futureProduct } = getExistingCusProducts({
product: cusProduct.product,
cusProducts: fullCus.customer_products,
internalEntityId: cusProduct.internal_entity_id,
});
let { curScheduledProduct: futureProduct } = getExistingCusProducts({
product: cusProduct.product,
cusProducts: fullCus.customer_products,
internalEntityId: cusProduct.internal_entity_id,
});
if (futureProduct) {
if (isMain) {
if (
cusProduct.canceled_at &&
ACTIVE_STATUSES.includes(cusProduct.status) &&
!expireImmediately
) {
throw new RecaseError({
message: `Product ${cusProduct.product.name} is already about to cancel at the end of cycle.`,
code: ErrCode.InvalidRequest,
statusCode: StatusCodes.BAD_REQUEST,
});
}
if (
futureProduct &&
!isFreeProduct(cusProductToPrices({ cusProduct: futureProduct }))
) {
throw new RecaseError({
message: `Please delete scheduled product ${futureProduct.product.name} first`,
code: ErrCode.InvalidRequest,
@@ -130,6 +149,7 @@ export const expireCusProduct = async ({
// 2. If expire at cycle end, just cancel subscriptions
if (!expireImmediately) {
// 1. Check if already canceled
await cancelCusProductSubscriptions({
cusProduct,
org,
@@ -175,6 +195,14 @@ export const expireCusProduct = async ({
return;
}
// Remove scheduled products first...
if (futureProduct) {
await CusProductService.delete({
db,
cusProductId: futureProduct.id,
});
}
logger.info(`Expiring current product: ${cusProduct.product.name}`);
await expireAndActivate({
req,

View File

@@ -143,13 +143,21 @@ export const handleUpdateBalances = async (req: any, res: any) => {
delete properties.balance;
for (const cusEnt of cusEnts) {
if (cusEnt.internal_feature_id !== feature!.internal_id!) {
continue;
}
let cusEntIntCount = cusEnt.entitlement.interval_count || 1;
let deductionIntCount = balance.interval_count || 1;
let intCountMatch = notNullish(balance.interval_count)
? cusEntIntCount === deductionIntCount
: true;
let intMatch = notNullish(balance.interval)
? balance.interval === cusEnt.entitlement.interval
: true;
if (
notNullish(balance.interval) &&
balance.interval !== cusEnt.entitlement.interval
cusEnt.internal_feature_id !== feature!.internal_id! ||
!intMatch ||
!intCountMatch
) {
continue;
}
@@ -173,6 +181,7 @@ export const handleUpdateBalances = async (req: any, res: any) => {
toDeduct,
properties,
interval: balance.interval,
intervalCount: balance.interval_count,
});
}
@@ -186,12 +195,18 @@ export const handleUpdateBalances = async (req: any, res: any) => {
// Handle unlimited
if (featureDeduction.unlimited) {
// Get one active cusEnt and set unlimited to true
const cusEnt = notNullish(interval)
? cusEnts.find(
(cusEnt) =>
? cusEnts.find((cusEnt) => {
let cusEntIntCount = cusEnt.entitlement.interval_count || 1;
let deductionIntCount = featureDeduction.intervalCount || 1;
return (
cusEnt.internal_feature_id === feature!.internal_id! &&
cusEnt.entitlement.interval === interval
)
cusEnt.entitlement.interval === interval &&
cusEntIntCount === deductionIntCount
);
})
: cusEnts.find(
(cusEnt) => cusEnt.internal_feature_id === feature!.internal_id!
);
@@ -218,9 +233,22 @@ export const handleUpdateBalances = async (req: any, res: any) => {
}
for (const cusEnt of cusEnts) {
let cusEntIntCount = cusEnt.entitlement.interval_count || 1;
let deductionIntCount = featureDeduction.intervalCount || 1;
let intCountMatch = notNullish(featureDeduction.intervalCount)
? cusEntIntCount === deductionIntCount
: true;
let intMatch = notNullish(featureDeduction.interval)
? featureDeduction.interval === cusEnt.entitlement.interval
: true;
if (
cusEnt.internal_feature_id !==
featureDeduction.feature!.internal_id!
featureDeduction.feature!.internal_id! ||
!intMatch ||
!intCountMatch
) {
continue;
}

View File

@@ -216,7 +216,7 @@ export const handleGetOtp = async (req: any, res: any) =>
// Generate API key for the OTP
const sandboxKey = await createKey({
db,
env: env || AppEnv.Sandbox,
env: AppEnv.Sandbox,
name: `Autumn Key CLI`,
orgId: cacheData.orgId,
prefix: "am_sk_test",
@@ -229,7 +229,7 @@ export const handleGetOtp = async (req: any, res: any) =>
const prodKey = await createKey({
db,
env: env || AppEnv.Live,
env: AppEnv.Live,
name: `Autumn Key CLI`,
orgId: cacheData.orgId,
prefix: "am_sk_live",

View File

@@ -27,6 +27,7 @@ export const sendOnboardingEmail = safeResend({
to: email,
subject: "Anything I can help with?",
body: getWelcomeEmailBody(firstName),
replyTo: "ayush@useautumn.com",
});
},
action: "send onboarding email",

View File

@@ -4,25 +4,26 @@ import { ErrCode } from "@/errors/errCodes.js";
import { clearOrgCache } from "../orgs/orgUtils/clearOrgCache.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { and, eq } from "drizzle-orm";
import { notNullish } from "@/utils/genUtils.js";
export class FeatureService {
static async list({
db,
orgId,
env,
showOnlyArchived = false,
archived,
}: {
db: DrizzleCli;
orgId: string;
env: AppEnv;
showOnlyArchived?: boolean;
archived?: boolean;
}) {
const features = await db.query.features.findMany({
where: (features, { eq, and }) =>
and(
eq(features.org_id, orgId),
eq(features.env, env),
eq(features.archived, showOnlyArchived)
notNullish(archived) ? eq(features.archived, archived!) : undefined
),
orderBy: (features, { desc }) => [desc(features.internal_id)],
@@ -208,5 +209,4 @@ export class FeatureService {
.delete(features)
.where(and(eq(features.org_id, orgId), eq(features.env, env)));
}
}

View File

@@ -27,12 +27,15 @@ featureRouter.get("", async (req: any, res: any) =>
res,
action: "list features",
handler: async () => {
const includeArchived = req.query.include_archived === "true";
let features = await FeatureService.list({
db: req.db,
orgId: req.orgId,
env: req.env,
showOnlyArchived: false,
archived: includeArchived ? undefined : false,
// showOnlyArchived: includeArchived ? undefined : false,
});
res
.status(200)
.json({ list: features.map((feature) => toAPIFeature({ feature })) });
@@ -119,7 +122,7 @@ featureRouter.post("/:feature_id", async (req: any, res: any) =>
}
if (apiFeature.credit_schema) {
newConfig.credit_schema = apiFeature.credit_schema.map((credit) => ({
newConfig.schema = apiFeature.credit_schema.map((credit) => ({
metered_feature_id: credit.metered_feature_id,
credit_amount: credit.credit_cost,
}));

View File

@@ -9,6 +9,8 @@ import {
Organization,
ProductItemFeatureType,
FeatureType,
FullCustomer,
UsagePriceConfig,
} from "@autumn/shared";
import { FeatureService } from "./FeatureService.js";
import { StatusCodes } from "http-status-codes";
@@ -16,6 +18,11 @@ import { generateFeatureDisplay } from "@/external/llm/llmUtils.js";
import { ProductService } from "../products/ProductService.js";
import { getCreditSystemsFromFeature } from "./creditSystemUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import {
cusProductsToCusPrices,
cusProductToPrices,
} from "../customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { priceToFeature } from "../products/prices/priceUtils/convertPrice.js";
export const validateFeatureId = (featureId: string) => {
if (!featureId.match(/^[a-zA-Z0-9_-]+$/)) {
@@ -77,9 +84,9 @@ export const validateCreditSystem = (config: CreditSystemConfig) => {
// Check if multiple of the same feature
const meteredFeatureIds = schema.map(
(schemaItem) => schemaItem.metered_feature_id,
(schemaItem) => schemaItem.metered_feature_id
);
console.log("Metered feature ids:", meteredFeatureIds);
// console.log("Metered feature ids:", meteredFeatureIds);
const uniqueMeteredFeatureIds = Array.from(new Set(meteredFeatureIds));
if (meteredFeatureIds.length !== uniqueMeteredFeatureIds.length) {
throw new RecaseError({
@@ -135,14 +142,14 @@ export const getObjectsUsingFeature = async ({
});
let entitlements = allEnts.filter(
(entitlement) => entitlement.internal_feature_id == feature.internal_id,
(entitlement) => entitlement.internal_feature_id == feature.internal_id
);
let linkedEntitlements = allEnts.filter(
(entitlement) => entitlement.entity_feature_id == feature.id,
(entitlement) => entitlement.entity_feature_id == feature.id
);
let prices = allPrices.filter(
(price) => (price.config as any).internal_feature_id == feature.internal_id,
(price) => (price.config as any).internal_feature_id == feature.internal_id
);
return { entitlements, prices, creditSystems, linkedEntitlements };
@@ -163,7 +170,7 @@ export const runSaveFeatureDisplayTask = async ({
try {
if (!process.env.ANTHROPIC_API_KEY) {
logger.warn(
"ANTHROPIC_API_KEY is not set, skipping feature display generation",
"ANTHROPIC_API_KEY is not set, skipping feature display generation"
);
return;
}
@@ -204,3 +211,29 @@ export const getCusFeatureType = ({ feature }: { feature: Feature }) => {
export const isCreditSystem = ({ feature }: { feature: Feature }) => {
return feature.type == FeatureType.CreditSystem;
};
export const isPaidContinuousUse = ({
feature,
fullCus,
}: {
feature: Feature;
fullCus: FullCustomer;
}) => {
let isContinuous = feature.config?.usage_type == FeatureUsageType.Continuous;
if (!isContinuous) {
return false;
}
let cusPrices = cusProductsToCusPrices({
cusProducts: fullCus.customer_products,
});
let hasPaid = cusPrices.some((cp) => {
let config = cp.price.config as UsagePriceConfig;
if (config.internal_feature_id == feature.internal_id) {
return true;
}
});
return hasPaid;
};

View File

@@ -9,11 +9,13 @@ import { handleFrontendReqError } from "@/utils/errorUtils.js";
export const handleCreateFeature = async (req: any, res: any) => {
try {
console.log("Trying to create feature");
const data = req.body;
let { db, orgId, env, logtail: logger } = req;
let parsedFeature = validateFeature(data);
let feature: Feature = {
const feature: Feature = {
archived: false,
internal_id: generateId("fe"),
org_id: orgId,
created_at: Date.now(),

View File

@@ -350,6 +350,17 @@ export const handleUpdateFeature = async (req: any, res: any) =>
}
}
const newConfig =
data.config !== undefined
? feature.type == FeatureType.CreditSystem
? validateCreditSystem(data.config)
: feature.type == FeatureType.Metered
? validateMeteredConfig(data.config)
: data.config
: feature.config;
console.log(`feature: ${feature.id}, new config:`, newConfig);
let updatedFeature = await FeatureService.update({
db: req.db,
id: featureId,
@@ -359,15 +370,10 @@ export const handleUpdateFeature = async (req: any, res: any) =>
id: data.id !== undefined ? data.id : feature.id,
name: data.name !== undefined ? data.name : feature.name,
type: data.type !== undefined ? data.type : feature.type,
archived: data.archived !== undefined ? data.archived : feature.archived,
archived:
data.archived !== undefined ? data.archived : feature.archived,
config: data.config !== undefined
? (feature.type == FeatureType.CreditSystem
? validateCreditSystem(data.config)
: feature.type == FeatureType.Metered
? validateMeteredConfig(data.config)
: data.config)
: feature.config,
config: newConfig,
},
});

View File

@@ -29,14 +29,14 @@ export const internalFeatureRouter: Router = express.Router();
internalFeatureRouter.get("", async (req: any, res: any) => {
try {
let { showArchived } = req.query;
if (showArchived !== undefined) {
// If showArchived is specified, use FeatureService.list with the parameter
let features = await FeatureService.list({
db: req.db,
orgId: req.orgId,
env: req.env,
showOnlyArchived: showArchived === 'true',
archived: showArchived === "true" ? true : false,
});
res.status(200).json({ features });
} else {
@@ -50,8 +50,6 @@ internalFeatureRouter.get("", async (req: any, res: any) => {
}
});
export const validateFeature = (data: any) => {
let featureType = data.type;
@@ -96,26 +94,23 @@ export const initNewFeature = ({
internalFeatureRouter.post("", async (req: any, res) => {
let data = req.body;
try {
let { db, orgId, env, logtail: logger } = req;
let parsedFeature = validateFeature(data);
let feature: Feature = {
const feature: Feature = {
archived: false,
internal_id: generateId("fe"),
org_id: orgId,
created_at: Date.now(),
env: env,
...parsedFeature,
};
let org = await OrgService.getFromReq(req);
let insertedData = await FeatureService.insert({
db,
data: feature,
logger,
});
await addTaskToQueue({
jobName: JobName.GenerateFeatureDisplay,
payload: {
@@ -123,7 +118,6 @@ internalFeatureRouter.post("", async (req: any, res) => {
org: org,
},
});
let insertedFeature =
insertedData && insertedData.length > 0 ? insertedData[0] : null;
res.status(200).json(insertedFeature);
@@ -132,60 +126,63 @@ internalFeatureRouter.post("", async (req: any, res) => {
}
});
internalFeatureRouter.get("/data/deletion_text/:feature_id", async (req: any, res) => {
try {
let { db } = req;
let { feature_id } = req.params;
// Get the feature first
let feature = await FeatureService.get({
db,
id: feature_id,
orgId: req.orgId,
env: req.env,
});
internalFeatureRouter.get(
"/data/deletion_text/:feature_id",
async (req: any, res) => {
try {
let { db } = req;
let { feature_id } = req.params;
if (!feature) {
return res.status(404).json({ error: "Feature not found" });
}
// Get the feature first
let feature = await FeatureService.get({
db,
id: feature_id,
orgId: req.orgId,
env: req.env,
});
// Use Drizzle query similar to ProductService.getDeletionText
let res_data = await db
.select({
productName: sql<string>`CASE WHEN ROW_NUMBER() OVER (ORDER BY ${products.created_at}) = 1 THEN ${products.name ?? "Product name not found"} ELSE NULL END`,
totalCount: sql<number>`COUNT(*) OVER ()`,
})
.from(products)
.innerJoin(
entitlements,
eq(products.internal_id, entitlements.internal_product_id)
)
.where(
and(
eq(entitlements.internal_feature_id, feature.internal_id!),
eq(products.env, req.env),
eq(products.org_id, req.orgId)
if (!feature) {
return res.status(404).json({ error: "Feature not found" });
}
// Use Drizzle query similar to ProductService.getDeletionText
let res_data = await db
.select({
productName: sql<string>`CASE WHEN ROW_NUMBER() OVER (ORDER BY ${products.created_at}) = 1 THEN ${products.name ?? "Product name not found"} ELSE NULL END`,
totalCount: sql<number>`COUNT(*) OVER ()`,
})
.from(products)
.innerJoin(
entitlements,
eq(products.internal_id, entitlements.internal_product_id)
)
)
.limit(1);
.where(
and(
eq(entitlements.internal_feature_id, feature.internal_id!),
eq(products.env, req.env),
eq(products.org_id, req.orgId)
)
)
.limit(1);
// If no products found, return explicit zero count
if (!res_data || res_data.length === 0) {
res.status(200).json({
productName: null,
totalCount: 0,
});
} else {
res.status(200).json({
productName: res_data[0]?.productName || null,
totalCount: Number(res_data[0]?.totalCount) || 0,
});
// If no products found, return explicit zero count
if (!res_data || res_data.length === 0) {
res.status(200).json({
productName: null,
totalCount: 0,
});
} else {
res.status(200).json({
productName: res_data[0]?.productName || null,
totalCount: Number(res_data[0]?.totalCount) || 0,
});
}
} catch (error) {
console.error("Failed to get feature deletion text", error);
res.status(500).send(error);
}
} catch (error) {
console.error("Failed to get feature deletion text", error);
res.status(500).send(error);
}
});
);
internalFeatureRouter.post("/:feature_id", handleUpdateFeature);
internalFeatureRouter.delete("/:featureId", handleDeleteFeature);

View File

@@ -11,6 +11,7 @@ import {
getFeatureNameWithCapital,
Organization,
Price,
ProductItemInterval,
UsagePriceConfig,
} from "@autumn/shared";
import {
@@ -24,6 +25,8 @@ import {
} from "../customers/cusProducts/cusPrices/cusPriceUtils.js";
import { getFeatureQuantity } from "../customers/cusProducts/cusProductUtils.js";
import { formatAmount } from "@/utils/formatUtils.js";
import { getIntervalString } from "../products/productUtils/productResponseUtils/getProductItemDisplay.js";
import { billingToItemInterval } from "../products/product-items/itemIntervalUtils.js";
const getSingularAndPlural = (feature: Feature) => {
const singular = getFeatureName({
@@ -72,10 +75,16 @@ export const formatFixedPrice = ({
const config = price.config as FixedPriceConfig;
const amount = formatAmount({ org, amount: config.amount });
const intervalStr = getIntervalString({
interval: billingToItemInterval(config.interval) as ProductItemInterval,
intervalCount: config.interval_count || 1,
prefix: "",
});
if (config.interval == BillingInterval.OneOff) {
return `${amount}`;
} else {
return `${amount} / ${config.interval}`;
return `${amount} / ${intervalStr}`;
}
};

View File

@@ -80,7 +80,7 @@ export const invoicesToResponse = ({
invoice: i,
withItems: false,
features: [],
}),
})
);
};
@@ -119,10 +119,33 @@ export const getInvoiceItems = async ({
} catch (error) {
logger.error(
`Failed to get invoice items for invoice ${stripeInvoice.id}`,
error,
error
);
return [];
}
return invoiceItems;
};
export const attachToInvoiceResponse = ({
invoice,
}: {
invoice?: Stripe.Invoice;
}) => {
if (!invoice) {
return undefined;
}
return {
status: invoice.status,
stripe_id: invoice.id,
hosted_invoice_url: invoice.hosted_invoice_url,
total: invoice.total,
currency: invoice.currency,
// id: invoice.id,
// stripe_id: invoice.stripe_id,
// status: invoice.status,
// created_at: invoice.created_at,
// updated_at: invoice.updated_at,
};
};

View File

@@ -63,6 +63,7 @@ export const getCurContUseItems = async ({
const finalProration = getProration({
now,
interval: price.config.interval!,
intervalCount: price.config.interval_count || 1,
anchorToUnix: sub.current_period_end * 1000,
})!;

View File

@@ -86,6 +86,7 @@ export const getItemsForCurProduct = async ({
const finalProration = getProration({
now,
interval: price.config.interval!,
intervalCount: price.config.interval_count || 1,
anchorToUnix: sub.current_period_end * 1000,
})!;

View File

@@ -51,6 +51,7 @@ import {
import { sortPricesByType } from "@/internal/products/prices/priceUtils/sortPriceUtils.js";
import { getMergeCusProduct } from "@/internal/customers/attach/attachFunctions/addProductFlow/getMergeCusProduct.js";
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
import { intervalsDifferent } from "@/internal/products/prices/priceUtils/priceIntervalUtils.js";
export const getDefaultPriceStr = ({
org,
@@ -83,6 +84,7 @@ export const getProration = ({
anchorToUnix,
now,
interval,
intervalCount,
}: {
proration?: {
start: number;
@@ -90,6 +92,7 @@ export const getProration = ({
};
anchorToUnix?: number;
interval: BillingInterval;
intervalCount: number;
now: number;
}) => {
if (!proration && !anchorToUnix) return undefined;
@@ -103,11 +106,16 @@ export const getProration = ({
let end = getAlignedIntervalUnix({
alignWithUnix: anchorToUnix!,
interval,
intervalCount,
now,
alwaysReturn: true,
});
let start = subtractBillingIntervalUnix(end!, interval);
let start = subtractBillingIntervalUnix({
unixTimestamp: end!,
interval,
intervalCount,
});
return {
start,
@@ -121,6 +129,7 @@ export const getItemsForNewProduct = async ({
now,
proration,
interval,
intervalCount,
anchorToUnix,
freeTrial,
stripeSubs,
@@ -137,6 +146,7 @@ export const getItemsForNewProduct = async ({
end: number;
};
interval?: BillingInterval;
intervalCount?: number;
anchorToUnix?: number;
freeTrial?: FreeTrial | null;
stripeSubs?: Stripe.Subscription[];
@@ -156,13 +166,27 @@ export const getItemsForNewProduct = async ({
const ent = getPriceEntitlement(price, newProduct.entitlements);
const billingType = getBillingType(price.config);
if (interval && price.config.interval !== interval) continue;
if (
interval &&
intervalsDifferent({
intervalA: {
interval: interval,
intervalCount: intervalCount,
},
intervalB: {
interval: price.config.interval,
intervalCount: price.config.interval_count,
},
})
)
continue;
const finalProration = getProration({
proration,
anchorToUnix,
now,
interval: price.config.interval!,
intervalCount: price.config.interval_count!,
});
if (isFixedPrice({ price })) {

View File

@@ -116,7 +116,7 @@ mainRouter.use(
},
identify: async (req: any) => {
return {
customerId: "user_123",
customerId: "onboarding_demo_user",
customerData: {
name: "Demo User",
email: "demo@useautumn.com",

View File

@@ -32,9 +32,11 @@ export const connectStripe = async ({
env: AppEnv;
}) => {
// 1. Check if key is valid
await checkKeyValid(apiKey);
let stripe = new Stripe(apiKey);
let account = await stripe.accounts.retrieve();
// 2. Disconnect existing webhook endpoints
@@ -47,6 +49,7 @@ export const connectStripe = async ({
// 3. Create webhook endpoint
let webhook = await createWebhookEndpoint(apiKey, env, orgId);
console.log("MADE IT HERE");
// 3. Return encrypted
if (env === AppEnv.Sandbox) {
@@ -96,7 +99,9 @@ export const connectAllStripe = async ({
// Get default currency from Stripe
let stripe = new Stripe(testApiKey);
let account = await stripe.accounts.retrieve();
if (nullish(defaultCurrency) && nullish(account.default_currency)) {
throw new RecaseError({
message: "Default currency not set",
@@ -107,7 +112,7 @@ export const connectAllStripe = async ({
defaultCurrency = account.default_currency;
}
} catch (error: any) {
console.error("Error checking stripe keys", error);
// console.error("Error checking stripe keys", error);
throw new RecaseError({
message: error.message || "Invalid Stripe API keys",
code: ErrCode.StripeKeyInvalid,

View File

@@ -77,12 +77,14 @@ orgRouter.post("/stripe", async (req: any, res) => {
logger,
});
console.log("Connecting Stripe");
await checkKeyValid(testApiKey);
await checkKeyValid(liveApiKey);
// Get default currency from Stripe
let stripe = new Stripe(testApiKey);
let account = await stripe.accounts.retrieve();
if (nullish(defaultCurrency) && nullish(account.default_currency)) {
throw new RecaseError({
message: "Default currency not set",
@@ -93,6 +95,14 @@ orgRouter.post("/stripe", async (req: any, res) => {
defaultCurrency = account.default_currency;
}
} catch (error: any) {
// if (error.message.includes("rk_***")) {
// throw new RecaseError({
// message: "Invalid Stripe restricted key. Please add the ",
// code: ErrCode.StripeKeyInvalid,
// statusCode: 500,
// });
// }
throw new RecaseError({
message: error.message || "Invalid Stripe API keys",
code: ErrCode.StripeKeyInvalid,

View File

@@ -265,12 +265,14 @@ export class ProductService {
orgId,
env,
version,
allowNotFound = false,
}: {
db: DrizzleCli;
idOrInternalId: string;
orgId: string;
env: AppEnv;
version?: number;
allowNotFound?: boolean;
}) {
let data = (await db.query.products.findFirst({
where: and(
@@ -298,7 +300,7 @@ export class ProductService {
parseFreeTrials({ product: data });
if (!data) {
// return null;
if (allowNotFound) return null;
throw new RecaseError({
message: `Product ${idOrInternalId} not found`,
code: ErrCode.ProductNotFound,

View File

@@ -23,28 +23,34 @@ import { addDays } from "date-fns";
import { getBillingType } from "@/internal/products/prices/priceUtils.js";
import { features } from "process";
export const entIntervalToTrialDuration = (interval: EntInterval) => {
export const entIntervalToTrialDuration = ({
interval,
intervalCount,
}: {
interval: EntInterval;
intervalCount: number;
}) => {
switch (interval) {
case EntInterval.Day:
return 1;
return intervalCount;
case EntInterval.Week:
return 7;
return intervalCount * 7;
case EntInterval.Month:
return 30;
return intervalCount * 30;
case EntInterval.Quarter:
return 90;
return intervalCount * 90;
case EntInterval.SemiAnnual:
return 180;
return intervalCount * 180;
case EntInterval.Year:
return 365;
return intervalCount * 365;
case EntInterval.Lifetime:
return 1000;
return intervalCount * 1000;
}
};
export const applyTrialToEntitlement = (
entitlement: EntitlementWithFeature,
freeTrial: FreeTrial | null,
freeTrial: FreeTrial | null
) => {
if (!freeTrial) return false;
@@ -54,7 +60,10 @@ export const applyTrialToEntitlement = (
if (entitlement.allowance_type === AllowanceType.Unlimited) return false;
const trialDays = freeTrial.length;
const entDays = entIntervalToTrialDuration(entitlement.interval!);
const entDays = entIntervalToTrialDuration({
interval: entitlement.interval!,
intervalCount: entitlement.interval_count || 1,
});
if (entDays && entDays > trialDays) {
return true;
@@ -65,7 +74,7 @@ export const applyTrialToEntitlement = (
export const addTrialToNextResetAt = (
nextResetAt: number,
freeTrial: FreeTrial | null,
freeTrial: FreeTrial | null
) => {
if (!freeTrial) return nextResetAt;
@@ -76,7 +85,7 @@ export const entsAreSame = (ent1: Entitlement, ent2: Entitlement) => {
// 1. Check if they have same internal_feature_id
if (ent1.internal_feature_id !== ent2.internal_feature_id) {
console.log(
`Internal feature ID different: ${ent1.internal_feature_id} !== ${ent2.internal_feature_id}`,
`Internal feature ID different: ${ent1.internal_feature_id} !== ${ent2.internal_feature_id}`
);
return false;
}
@@ -84,7 +93,7 @@ export const entsAreSame = (ent1: Entitlement, ent2: Entitlement) => {
// 2. Check if they have same allowance type
if (ent1.allowance_type !== ent2.allowance_type) {
console.log(
`Allowance type different: ${ent1.allowance_type} !== ${ent2.allowance_type}`,
`Allowance type different: ${ent1.allowance_type} !== ${ent2.allowance_type}`
);
return false;
}
@@ -94,6 +103,10 @@ export const entsAreSame = (ent1: Entitlement, ent2: Entitlement) => {
condition: ent1.interval !== ent2.interval,
message: `Interval different: ${ent1.interval} !== ${ent2.interval}`,
},
intervalCount: {
condition: ent1.interval_count !== ent2.interval_count,
message: `Interval count different: ${ent1.interval_count} !== ${ent2.interval_count}`,
},
allowance: {
condition:
ent1.allowance_type !== AllowanceType.Unlimited &&
@@ -113,7 +126,8 @@ export const entsAreSame = (ent1: Entitlement, ent2: Entitlement) => {
message: `Usage limit different: ${ent1.usage_limit} !== ${ent2.usage_limit}`,
},
rollover: {
condition: JSON.stringify(ent1.rollover) !== JSON.stringify(ent2.rollover),
condition:
JSON.stringify(ent1.rollover) !== JSON.stringify(ent2.rollover),
message: `Rollover different: ${ent1.rollover} !== ${ent2.rollover}`,
},
};
@@ -136,7 +150,7 @@ export const entsAreSame = (ent1: Entitlement, ent2: Entitlement) => {
export const getEntRelatedPrice = (
entitlement: Entitlement,
prices: Price[],
allowFeatureMatch = false,
allowFeatureMatch = false
) => {
return prices.find((price) => {
if (price.config?.type === PriceType.Fixed) {
@@ -158,10 +172,10 @@ export const getEntRelatedPrice = (
export const getEntitlementsForProduct = (
product: FullProduct,
entitlements: EntitlementWithFeature[],
entitlements: EntitlementWithFeature[]
) => {
return entitlements.filter(
(ent) => ent.internal_product_id === product.internal_id,
(ent) => ent.internal_product_id === product.internal_id
);
};
@@ -174,7 +188,7 @@ export const getEntsWithFeature = ({
}) => {
return ents.map((ent) => {
let feature = features.find(
(f) => f.internal_id === ent.internal_feature_id,
(f) => f.internal_id === ent.internal_feature_id
);
if (!feature) {
throw new RecaseError({

View File

@@ -1,9 +1,7 @@
import { routeHandler } from "@/utils/routerUtils.js";
import { ProductService } from "@/internal/products/ProductService.js";
import { ExtendedRequest, ExtendedResponse } from "@/utils/models/Request.js";
import { CusService } from "@/internal/customers/CusService.js";
import { sortFullProducts } from "../productUtils/sortProductUtils.js";
import { toPricecnProduct } from "../pricecn/pricecnUtils.js";
import { getProductResponse } from "../productUtils/productResponseUtils/getProductResponse.js";
import { getCusWithCache } from "@/internal/customers/cusCache/getCusWithCache.js";
@@ -38,14 +36,6 @@ export const handleListProductsBeta = async (req: any, res: any) =>
env,
logger: req.logger,
});
// return await CusService.getFull({
// db,
// orgId: org.id,
// env,
// idOrInternalId: customerId as string,
// allowNotFound: true,
// entityId,
// });
})(),
]);

View File

@@ -17,6 +17,7 @@ import { addTaskToQueue } from "@/queue/queueUtils.js";
import { JobName } from "@/queue/JobName.js";
import { productsAreSame } from "../../productUtils/compareProductUtils.js";
import { initProductInStripe } from "../../productUtils.js";
import { handleCreateProduct } from "../handleCreateProduct.js";
export const handleUpdateProductV2 = async (req: any, res: any) =>
routeHandler({
@@ -25,7 +26,7 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
action: "Update product",
handler: async () => {
const { productId } = req.params;
const { version } = req.query;
const { version, upsert, disable_version } = req.query;
const { orgId, env, logger, db } = req;
const [features, org, fullProduct, rewardPrograms] = await Promise.all([
@@ -37,6 +38,7 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
orgId,
env,
version: version ? parseInt(version) : undefined,
allowNotFound: upsert == "true",
}),
RewardProgramService.getByProductId({
db,
@@ -47,6 +49,12 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
]);
if (!fullProduct) {
console.log("Upserting:", upsert);
if (upsert == "true") {
await handleCreateProduct(req, res);
return;
}
throw new RecaseError({
message: "Product not found",
code: ErrCode.ProductNotFound,
@@ -75,6 +83,15 @@ export const handleUpdateProductV2 = async (req: any, res: any) =>
let itemsExist = notNullish(req.body.items);
if (cusProductExists && itemsExist) {
if (disable_version == "true") {
throw new RecaseError({
message: "Cannot auto save product as there are existing customers",
code: ErrCode.InvalidRequest,
statusCode: 400,
});
return;
}
const { itemsSame, freeTrialsSame } = productsAreSame({
newProductV2: req.body,
curProductV1: fullProduct,

View File

@@ -42,7 +42,13 @@ const productDetailsSame = (prod1: Product, prod2: UpdateProduct) => {
if (notNullish(prod2.is_default) && prod1.is_default != prod2.is_default) {
return false;
}
console.log("prod1.archived", prod1.archived, prod2.archived, notNullish(prod2.archived), prod1.archived !== prod2.archived);
// console.log(
// "prod1.archived",
// prod1.archived,
// prod2.archived,
// notNullish(prod2.archived),
// prod1.archived !== prod2.archived
// );
if (notNullish(prod2.archived) && prod1.archived !== prod2.archived) {
return false;
}
@@ -71,7 +77,7 @@ const updateStripeProductNames = async ({
});
let stripeProdId = curProduct.processor?.id;
if (!stripeProdId) {
if (!stripeProdId || !newName) {
return;
}

View File

@@ -59,6 +59,7 @@ export const handleVersionProductV2 = async ({
orgId: org.id,
env: latestProduct.env as AppEnv,
processor: latestProduct.processor,
baseVariantId: latestProduct.base_variant_id,
});
// Validate product items...
@@ -114,12 +115,16 @@ export const handleVersionProductV2 = async ({
});
}
await addTaskToQueue({
jobName: JobName.DetectBaseVariant,
payload: {
curProduct: newProduct,
},
});
// await addTaskToQueue({
// jobName: JobName.DetectBaseVariant,
// payload: {
// curProduct: {
// ...newProduct,
// // prices: customPrices,
// // entitlements: getEntsWithFeature({ ents: customEnts, features }),
// },
// },
// });
await initProductInStripe({
db,

View File

@@ -27,14 +27,16 @@ productRouter.get("/data", async (req: any, res) => {
try {
let { db } = req;
const allVersions = req.query.all_versions === "true";
const [products, features, org, coupons, rewardPrograms] =
await Promise.all([
ProductService.listFull({
db,
orgId: req.orgId,
env: req.env,
returnAll: true,
archived: false,
returnAll: allVersions,
}),
FeatureService.getFromReq(req),
OrgService.getFromReq(req),
@@ -77,7 +79,7 @@ productRouter.post("/data", async (req: any, res) => {
db,
orgId: req.orgId,
env: req.env,
returnAll: true,
// returnAll: true,
archived: showArchived,
}),
FeatureService.getFromReq(req),
@@ -116,8 +118,17 @@ productRouter.get("/counts", async (req: any, res) => {
// returnAll: true,
});
const latestVersion = req.query.latest_version === "true";
let counts = await Promise.all(
products.map(async (product) => {
if (latestVersion) {
return CusProdReadService.getCounts({
db,
internalProductId: product.internal_id,
});
}
return CusProdReadService.getCountsForAllVersions({
db,
productId: product.id,

View File

@@ -22,7 +22,7 @@ import { isPriceItem } from "../product-items/productItemUtils/getItemType.js";
import { isFeaturePriceItem } from "../product-items/productItemUtils/getItemType.js";
import { cusProductToProduct } from "@/internal/customers/cusProducts/cusProductUtils/convertCusProduct.js";
import { isProductUpgrade } from "../productUtils.js";
import { getFirstInterval } from "../prices/priceUtils/priceIntervalUtils.js";
import { getLargestInterval } from "../prices/priceUtils/priceIntervalUtils.js";
import { DrizzleCli } from "@/db/initDrizzle.js";
import { getFreeTrialAfterFingerprint } from "../free-trials/freeTrialUtils.js";
@@ -388,7 +388,7 @@ export const toPricecnProduct = async ({
let baseVariant = null;
if (fullProduct.base_variant_id) {
baseVariant = otherProducts.find(
(p) => p.id == fullProduct.base_variant_id,
(p) => p.id == fullProduct.base_variant_id
);
}
@@ -402,7 +402,8 @@ export const toPricecnProduct = async ({
baseVariant ||
otherProducts.some((p) => p.base_variant_id == product.id)
) {
intervalGroup = getFirstInterval({ prices: fullProduct.prices });
let intervalSet = getLargestInterval({ prices: fullProduct.prices });
intervalGroup = intervalSet?.interval;
}
let trialAvailable = false;

Some files were not shown because too many files have changed in this diff Show More