working on tests

This commit is contained in:
John Yeo
2025-11-07 16:52:17 +00:00
332 changed files with 24814 additions and 2001 deletions

1
.gitignore vendored
View File

@@ -113,3 +113,4 @@ interview
/server/scripts/
credentials/
server/chrome

141
bun.lock
View File

@@ -25,6 +25,7 @@
"@autumn/shared": "workspace:*",
"chalk": "^5.3.0",
"dotenv": "^16.5.0",
"drizzle-orm": "^0.44.7",
"inquirer": "^12.6.3",
"ora": "^9.0.0",
"p-limit": "^7.2.0",
@@ -98,6 +99,8 @@
"hono": "^4.9.9",
"hono-rate-limiter": "^0.4.2",
"http-status-codes": "^2.3.0",
"ink": "^6.3.1",
"ink-spinner": "^5.0.0",
"ioredis": "^5.5.0",
"ksuid": "^3.0.0",
"lodash-es": "^4.17.21",
@@ -105,6 +108,7 @@
"mime-detect": "^1.3.0",
"nanoid": "^5.1.6",
"openai": "^4.85.2",
"p-limit": "^7.2.0",
"pg": "^8.13.1",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
@@ -123,6 +127,7 @@
"zod": "^3.25.23",
},
"devDependencies": {
"@types/bun": "^1.3.1",
"@types/chai": "^5.0.1",
"@types/chai-http": "^3.0.5",
"@types/cors": "^2.8.19",
@@ -280,6 +285,8 @@
"@ai-sdk/ui-utils": ["@ai-sdk/ui-utils@1.2.11", "", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "zod-to-json-schema": "^3.24.1" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w=="],
"@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.2.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-mkOh+Wwawzuf5wa30bvc4nA+Qb6DIrGWgBhRR/Pw4T9nsgYait8izvXkNyU78D6Wcu3Z+KUdwCmLCxlWjEotYA=="],
"@amplitude/analytics-browser": ["@amplitude/analytics-browser@2.27.0", "", { "dependencies": { "@amplitude/analytics-core": "^2.28.0", "@amplitude/plugin-autocapture-browser": "^1.15.3", "@amplitude/plugin-network-capture-browser": "^1.6.9", "@amplitude/plugin-page-view-tracking-browser": "^2.5.3", "@amplitude/plugin-web-vitals-browser": "^0.1.0-beta.31", "tslib": "^2.4.1" } }, "sha512-1LBCLmnr7aUpLtOp64lpr8GzN3vPKM0fwiM/7tWJ9XU9/GKA+k3CUSjI8OdERKrw2yVywujoAVQo4anGZXYIDA=="],
"@amplitude/analytics-client-common": ["@amplitude/analytics-client-common@2.4.8", "", { "dependencies": { "@amplitude/analytics-connector": "^1.4.8", "@amplitude/analytics-core": "^2.28.0", "@amplitude/analytics-types": "^2.10.0", "tslib": "^2.4.1" } }, "sha512-cSm9Q+qcLy65kV2MnD7WlKrFMDOkj14dHM2YQn5njUrSXQljyjtYCss+HVzgIlWPSDgA5v6dDeHwxzZl3VSggw=="],
@@ -1608,9 +1615,11 @@
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"ansi-escapes": ["ansi-escapes@7.1.1", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-Zhl0ErHcSRUaVfGUeUdDuLgpkEo8KIFjB4Y9uAc46ScOpdDiU1Dbyplh7qWJeJ/ZHpbyMSM26+X3BySgnIz40Q=="],
"ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
@@ -1640,6 +1649,8 @@
"attr-accept": ["attr-accept@2.2.5", "", {}, "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ=="],
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
"autumn-js": ["autumn-js@0.1.40", "", { "dependencies": { "query-string": "^9.2.2", "rou3": "^0.6.1", "swr": "^2.3.3", "zod": "^4.0.0" }, "peerDependencies": { "better-auth": "^1.3.17", "better-call": "^1.0.12", "convex": "^1.25.4" }, "optionalPeers": ["better-auth", "better-call"] }, "sha512-nAmyFJLOQqKosb8MHv09rB2pma8LyOHWsuYtrjXND+2LM51vToco1mweLIYIs/aX33iLAVUxfpXEEt8P3UYoxw=="],
"axios": ["axios@1.12.2", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw=="],
@@ -1754,10 +1765,14 @@
"clean-css": ["clean-css@5.3.3", "", { "dependencies": { "source-map": "~0.6.0" } }, "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg=="],
"cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="],
"cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
"cli-spinners": ["cli-spinners@3.3.0", "", {}, "sha512-/+40ljC3ONVnYIttjMWrlL51nItDAbBrq2upN8BPyvGU/2n5Oxw3tbNwORCaNuNqLJnxGqOfjUuhsv7l5Q4IsQ=="],
"cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="],
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
@@ -1776,6 +1791,8 @@
"cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
@@ -1802,6 +1819,8 @@
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"convert-to-spaces": ["convert-to-spaces@2.0.1", "", {}, "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ=="],
"convex": ["convex@1.28.0", "", { "dependencies": { "esbuild": "0.25.4", "prettier": "^3.0.0" }, "peerDependencies": { "@auth0/auth0-react": "^2.0.1", "@clerk/clerk-react": "^4.12.8 || ^5.0.0", "react": "^18.0.0 || ^19.0.0-0 || ^19.0.0" }, "optionalPeers": ["@auth0/auth0-react", "@clerk/clerk-react", "react"], "bin": { "convex": "bin/main.js" } }, "sha512-40FgeJ/LxP9TxnkDDztU/A5gcGTdq1klcTT5mM0Ak+kSlQiDktMpjNX1TfkWLxXaE3lI4qvawKH95v2RiYgFxA=="],
"cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
@@ -1942,7 +1961,7 @@
"drizzle-kit": ["drizzle-kit@0.31.5", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-+CHgPFzuoTQTt7cOYCV6MOw2w8vqEn/ap1yv4bpZOWL03u7rlVRQhUY0WYT3rHsgVTXwYQDZaSUJSQrMBUKuWg=="],
"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-orm": ["drizzle-orm@0.44.7", "", { "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": "*", "@upstash/redis": ">=1.34.7", "@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", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ=="],
"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=="],
@@ -1954,7 +1973,7 @@
"electron-to-chromium": ["electron-to-chromium@1.5.237", "", {}, "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
@@ -1968,6 +1987,8 @@
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
"error-causes": ["error-causes@3.0.2", "", {}, "sha512-i0B8zq1dHL6mM85FGoxaJnVtx6LD5nL2v0hlpGdntg5FOSyzQ46c9lmz5qx0xRS2+PWHGOHcYxGIBC5Le2dRMw=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
@@ -2226,10 +2247,16 @@
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"indent-string": ["indent-string@5.0.0", "", {}, "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"ink": ["ink@6.3.1", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.2.0", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.6.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.39.10", "indent-string": "^5.0.0", "is-in-ci": "^2.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^6.1.2" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-3wGwITGrzL6rkWsi2gEKzgwdafGn4ZYd3u4oRp+sOPvfoxEHlnoB5Vnk9Uy5dMRUhDOqF3hqr4rLQ4lEzBc2sQ=="],
"ink-spinner": ["ink-spinner@5.0.0", "", { "dependencies": { "cli-spinners": "^2.7.0" }, "peerDependencies": { "ink": ">=4.0.0", "react": ">=18.0.0" } }, "sha512-EYEasbEjkqLGyPOUc8hBJZNuC5GvXGMLu0w5gdTNskPc7Izc5vO3tdQEYnzvshucyGCBXc86ig0ujXPMWaQCdA=="],
"input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="],
"inquirer": ["inquirer@12.10.0", "", { "dependencies": { "@inquirer/ansi": "^1.0.1", "@inquirer/core": "^10.3.0", "@inquirer/prompts": "^7.9.0", "@inquirer/type": "^3.0.9", "mute-stream": "^2.0.0", "run-async": "^4.0.5", "rxjs": "^7.8.2" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-K/epfEnDBZj2Q3NMDcgXWZye3nhSPeoJnOh8lcKWrldw54UEZfS4EmAMsAsmVbl7qKi+vjAsy39Sz4fbgRMewg=="],
@@ -2252,12 +2279,14 @@
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-in-browser": ["is-in-browser@1.1.3", "", {}, "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g=="],
"is-in-ci": ["is-in-ci@2.0.0", "", { "bin": { "is-in-ci": "cli.js" } }, "sha512-cFeerHriAnhrQSbpAxL37W1wcJKUUX07HyLWZCW1URJT/ra3GyUTzBgUnh24TMVfNTV2Hij2HLxkPHFZfOZy5w=="],
"is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="],
"is-ip": ["is-ip@5.0.1", "", { "dependencies": { "ip-regex": "^5.0.0", "super-regex": "^0.2.0" } }, "sha512-FCsGHdlrOnZQcp0+XT5a+pYowf33itBalCl+7ovNXC/7o5BhIpG14M3OrpPPdBSIQJCm+0M5+9mO7S9VVTTCFw=="],
@@ -2568,6 +2597,8 @@
"pascal-case": ["pascal-case@3.1.2", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g=="],
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -2720,6 +2751,8 @@
"react-promise-suspense": ["react-promise-suspense@0.3.4", "", { "dependencies": { "fast-deep-equal": "^2.0.1" } }, "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ=="],
"react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="],
"react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
@@ -2832,7 +2865,7 @@
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
@@ -2842,6 +2875,8 @@
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"socket.io": ["socket.io@4.8.1", "", { "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" } }, "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg=="],
@@ -2872,6 +2907,8 @@
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
"standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="],
@@ -2988,7 +3025,7 @@
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
"type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="],
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
@@ -3068,13 +3105,15 @@
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"widest-line": ["widest-line@5.0.0", "", { "dependencies": { "string-width": "^7.0.0" } }, "sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA=="],
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"workerpool": ["workerpool@9.3.4", "", {}, "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg=="],
"wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
@@ -3106,6 +3145,8 @@
"yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="],
"yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="],
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"zod-openapi": ["zod-openapi@5.4.3", "", { "peerDependencies": { "zod": "^3.25.74 || ^4.0.0" } }, "sha512-6kJ/gJdvHZtuxjYHoMtkl2PixCwRuZ/s79dVkEr7arHvZGXfx7Cvh53X3HfJ5h9FzGelXOXlnyjwfX0sKEPByw=="],
@@ -3140,6 +3181,10 @@
"@anthropic-ai/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="],
"@autumn/server/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=="],
"@autumn/shared/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=="],
"@autumn/vite/@types/node": ["@types/node@22.18.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ=="],
"@autumn/vite/date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="],
@@ -3532,6 +3577,10 @@
"@hyperdx/node-opentelemetry/ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="],
"@inquirer/core/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"@inquirer/core/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"@inquirer/external-editor/iconv-lite": ["iconv-lite@0.7.0", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@@ -3936,6 +3985,10 @@
"bun-types/@types/node": ["@types/node@24.8.1", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q=="],
"cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
"cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -3972,12 +4025,20 @@
"finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"gaxios/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"html-minifier-terser/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"ink/cli-cursor": ["cli-cursor@4.0.0", "", { "dependencies": { "restore-cursor": "^4.0.0" } }, "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg=="],
"ink/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"ink-spinner/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
"micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"mocha/log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="],
@@ -4018,6 +4079,8 @@
"react-promise-suspense/fast-deep-equal": ["fast-deep-equal@2.0.1", "", {}, "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w=="],
"react-reconciler/scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"react-router/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"recaseai/@types/node": ["@types/node@22.18.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ=="],
@@ -4030,12 +4093,16 @@
"renderkid/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
"send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
"serialize-error/type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="],
"socket.io/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"socket.io-adapter/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
@@ -4044,6 +4111,12 @@
"socket.io-parser/debug": ["debug@4.3.7", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ=="],
"stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"string-width-cjs/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -4062,11 +4135,13 @@
"type-is/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"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/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -4536,9 +4611,13 @@
"@hyperdx/node-opentelemetry/ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@inquirer/core/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"@inquirer/core/wrap-ansi/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"@inquirer/core/wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@opentelemetry/auto-instrumentations-node/@opentelemetry/instrumentation/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.202.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-fTBjMqKCfotFWfLzaKyhjLvyEyq5vDKTTFfBmx21btv3gvy8Lq6N5Dh2OzqeuN4DjtpSvNT1uNVfg08eD2Rfxw=="],
@@ -4700,10 +4779,20 @@
"bun-types/@types/node/undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
"cli-truncate/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
"cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"cloudflare/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="],
"concurrently/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"concurrently/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"convex/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.4", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q=="],
@@ -4760,6 +4849,8 @@
"engine.io/@types/node/undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
"eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"eslint/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
@@ -4770,6 +4861,8 @@
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"ink/cli-cursor/restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
"mocha/log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"mocha/log-symbols/is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="],
@@ -4832,9 +4925,15 @@
"type-is/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
@@ -5060,12 +5159,20 @@
"@hyperdx/node-opentelemetry/@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node/@opentelemetry/propagator-jaeger": ["@opentelemetry/propagator-jaeger@1.30.1", "", { "dependencies": { "@opentelemetry/core": "1.30.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg=="],
"@hyperdx/node-opentelemetry/ora/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@hyperdx/node-opentelemetry/ora/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"@hyperdx/node-opentelemetry/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="],
"@hyperdx/node-opentelemetry/ora/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@inquirer/core/wrap-ansi/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"@inquirer/core/wrap-ansi/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"@inquirer/core/wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/resources/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
"@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base/@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.28.0", "", {}, "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA=="],
@@ -5080,6 +5187,10 @@
"css-select/domutils/dom-serializer/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="],
"ink/cli-cursor/restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"mocha/log-symbols/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"mocha/log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"nodemon/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -5090,12 +5201,8 @@
"react-email/ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
"react-email/ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"recaseai/ora/log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="],
"recaseai/ora/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"renderkid/htmlparser2/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="],
"tsc-alias/chokidar/readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@@ -5188,8 +5295,6 @@
"@hyperdx/node-opentelemetry/ora/cli-cursor/restore-cursor/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"@hyperdx/node-opentelemetry/ora/cli-cursor/restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"@sentry/node/@opentelemetry/instrumentation-mysql/@types/mysql/@types/node/undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],
"@sentry/node/@opentelemetry/instrumentation-pg/@types/pg/@types/node/undici-types": ["undici-types@7.14.0", "", {}, "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA=="],

View File

@@ -12,6 +12,7 @@
"@autumn/shared": "workspace:*",
"chalk": "^5.3.0",
"dotenv": "^16.5.0",
"drizzle-orm": "^0.44.7",
"inquirer": "^12.6.3",
"ora": "^9.0.0",
"p-limit": "^7.2.0"

View File

@@ -21,6 +21,7 @@ fi
# 'server/tests/balances/track/legacy' \
# 'server/tests/balances/check/basic' \
# 'server/tests/balances/check/credit-systems' \
# 'server/tests/balances/check/misc' \
BUN_PARALLEL_COMPACT \
'server/tests/attach/basic' \

View File

@@ -12,17 +12,15 @@ if [[ "$1" == *"setup"* ]]; then
BUN_SETUP
fi
# These tests still use Mocha - will be migrated later
MOCHA_CMD \
'tests/attach/migrations/*.ts' \
'tests/attach/newVersion/*.ts' \
'tests/attach/upgradeOld/*.ts' \
'tests/attach/others/*.ts' \
'tests/attach/updateEnts/*.ts' \
'tests/advanced/check/*.ts'
MOCHA_CMD 'tests/attach/prepaid/*.ts' \
'tests/interval/upgrade/*.ts' \
'tests/interval/multiSub/*.ts'
BUN_PARALLEL_COMPACT \
'server/tests/attach/migrations' \
'server/tests/attach/others' \
# 'server/tests/attach/newVersion' \
# 'server/tests/attach/upgradeOld' \
# 'server/tests/attach/updateEnts' \
# 'server/tests/advanced/check' \
# 'server/tests/attach/prepaid' \
# 'server/tests/interval/upgrade' \
# 'server/tests/interval/multiSub' \
# --max=6

View File

@@ -12,10 +12,10 @@ if [[ "$1" == *"setup"* ]]; then
BUN_SETUP
fi
# These tests still use Mocha - will be migrated later
MOCHA_CMD 'tests/contUse/entities/*.ts'
MOCHA_CMD 'tests/contUse/update/*.ts'
MOCHA_CMD 'tests/contUse/track/*.ts'
MOCHA_CMD 'tests/contUse/roles/*.ts'
BUN_PARALLEL_COMPACT \
'server/tests/contUse/entities' \
'server/tests/contUse/update' \
'server/tests/contUse/track' \
'server/tests/contUse/roles' \
--max=6

View File

@@ -12,20 +12,18 @@ if [[ "$1" == *"setup"* ]]; then
BUN_SETUP
fi
# These tests still use Mocha - will be migrated later
MOCHA_CMD 'tests/merged/group/*.ts'
MOCHA_CMD 'tests/merged/add/*.ts' \
'tests/merged/downgrade/*.ts' \
'tests/merged/prepaid/*.ts' \
'tests/merged/separate/*.ts' \
'tests/merged/upgrade/*.ts' \
'tests/merged/trial/*.ts'
MOCHA_CMD 'tests/merged/addOn/*.ts' \
'tests/core/cancel/*.ts' \
'tests/core/multiAttach/*.ts' \
'tests/core/multiAttach/multiInvoice/*.ts' \
'tests/core/multiAttach/multiUpgrade/*.ts'
BUN_PARALLEL_COMPACT \
'server/tests/merged/group' \
'server/tests/merged/add' \
'server/tests/merged/downgrade' \
'server/tests/merged/prepaid' \
'server/tests/merged/separate' \
'server/tests/merged/upgrade' \
'server/tests/merged/trial' \
'server/tests/merged/addOn' \
'server/tests/core/cancel' \
'server/tests/core/multiAttach' \
'server/tests/core/multiAttach/multiInvoice' \
'server/tests/core/multiAttach/multiUpgrade' \
--max=6

View File

@@ -12,18 +12,15 @@ if [[ "$1" == *"setup"* ]]; then
BUN_SETUP
fi
# These tests still use Mocha - will be migrated later
# Note: advanced/multiFeature, advanced/rollovers, advanced/customInterval,
# advanced/usageLimit still use Mocha (not migrated yet)
MOCHA_CMD 'tests/advanced/multiFeature/*.ts' \
'tests/advanced/coupons/*.ts' \
'tests/attach/updateQuantity/*.ts' \
'tests/advanced/referrals/*.ts' \
'tests/advanced/referrals/paid/*.ts' \
'tests/advanced/rollovers/*.ts' \
'tests/advanced/customInterval/*.ts'
MOCHA_CMD 'tests/attach/multiProduct/*.ts' \
'tests/advanced/usageLimit/*.ts'
MOCHA_CMD 'tests/advanced/usage/*.ts'
BUN_PARALLEL_COMPACT \
'server/tests/advanced/coupons' \
'server/tests/attach/updateQuantity' \
'server/tests/advanced/referrals' \
'server/tests/advanced/referrals/paid' \
'server/tests/attach/multiProduct' \
'server/tests/advanced/usage' \
--max=6

42
server/checkFeatures.ts Normal file
View File

@@ -0,0 +1,42 @@
import dotenv from "dotenv";
dotenv.config();
import { AppEnv } from "@autumn/shared";
import { initDrizzle } from "@/db/initDrizzle.js";
import { FeatureService } from "@/internal/features/FeatureService.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import { StripeAccountService } from "@/internal/stripe/StripeAccountService.js";
const orgSlug = process.env.TESTS_ORG || "test-debug|org_2sWv2S8LJ9iaTjLI6UtNsfL88Kt";
async function main() {
const { db, client } = initDrizzle();
const org = await OrgService.getBySlug({ db, slug: orgSlug });
console.log("\n=== ORG ===");
console.log("ID:", org.id);
console.log("Slug:", org.slug);
console.log("Name:", org.name);
console.log("\n=== STRIPE CONFIG ===");
console.log("test_stripe_connect:", org.test_stripe_connect);
console.log("live_stripe_connect:", org.live_stripe_connect);
const features = await FeatureService.list({
db,
orgId: org.id,
env: AppEnv.Sandbox,
});
console.log("\n=== FEATURES ===");
console.log("Total features:", features.length);
for (const feature of features) {
console.log(`- ${feature.id} (${feature.type})`, feature.usage_type ? `usage_type: ${feature.usage_type}` : '');
if (feature.id === 'messages') {
console.log("\n Full messages feature:", JSON.stringify(feature, null, 2));
}
}
await client.end();
}
main();

View File

@@ -13,7 +13,13 @@
"cron": "bun src/cron.ts",
"check": "bun src/check.ts",
"build": "bun build ./src/index.ts ./src/workers.ts ./src/cron.ts --outdir dist --target bun",
"build:check": "tsc -b tsconfig.build.json"
"build:check": "tsc -b tsconfig.build.json",
"t": "bun tests/testRunner/runParallelGroupsV3.ts",
"parallel-tests": "bun tests/testRunner/runParallelGroupsV3.ts",
"parallel-tests:v1": "bun tests/testRunner/runParallelGroups.ts",
"parallel-tests:verbose": "bun tests/testRunner/runParallelGroups.ts --verbose",
"parallel-tests:debug": "bun tests/testRunner/runParallelGroups.ts --debug",
"clear-master": "bun tests/clearMasterOrg.ts"
},
"mocha": {
"node-option": [
@@ -82,6 +88,8 @@
"hono": "^4.9.9",
"hono-rate-limiter": "^0.4.2",
"http-status-codes": "^2.3.0",
"ink": "^6.3.1",
"ink-spinner": "^5.0.0",
"ioredis": "^5.5.0",
"ksuid": "^3.0.0",
"lodash-es": "^4.17.21",
@@ -89,6 +97,7 @@
"mime-detect": "^1.3.0",
"nanoid": "^5.1.6",
"openai": "^4.85.2",
"p-limit": "^7.2.0",
"pg": "^8.13.1",
"pino": "^9.6.0",
"pino-pretty": "^13.0.0",
@@ -107,6 +116,7 @@
"zod": "^3.25.23"
},
"devDependencies": {
"@types/bun": "^1.3.1",
"@types/chai": "^5.0.1",
"@types/chai-http": "^3.0.5",
"@types/cors": "^2.8.19",

View File

@@ -22,11 +22,11 @@ BUN_SETUP="$BUN_CMD tests/setupMain.ts"
# Test runner functions (using new TypeScript runner)
BUN_PARALLEL() {
cd "$PROJECT_ROOT" && $BUN_CMD scripts/testScripts/runTests.ts "$@"
cd "$PROJECT_ROOT" && $BUN_CMD server/tests/testRunner/runTests.ts "$@"
}
BUN_PARALLEL_COMPACT() {
cd "$PROJECT_ROOT" && $BUN_CMD scripts/testScripts/runTests.ts "$@" --compact
cd "$PROJECT_ROOT" && $BUN_CMD server/tests/testRunner/runTests.ts "$@" --compact
}
# Mocha command (for tests not yet migrated)

24
server/shell/parallel.sh Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/bash
# Parallel Test Runner
# Runs all test groups in parallel, each with its own dedicated org
# Source shared configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/config.sh"
# Check for required environment variables
if [ -z "$TEST_ORG_SECRET_KEY" ]; then
echo "Error: TEST_ORG_SECRET_KEY environment variable is required"
echo ""
echo "This should be the secret key of your platform organization"
echo "that has access to create/delete test organizations."
echo ""
echo "Add it to your server/.env file:"
echo " TEST_ORG_SECRET_KEY=am_sk_test_..."
exit 1
fi
# Run parallel test groups
echo "Starting parallel test runner..."
cd "$PROJECT_ROOT" && $BUN_CMD server/tests/testRunner/runParallelGroups.ts

View File

@@ -24,6 +24,7 @@ import type {
Customer,
UsageParams,
} from "autumn-js";
import { defaultApiVersion } from "tests/constants.js";
export default class AutumnError extends Error {
message: string;
@@ -70,7 +71,7 @@ export class AutumnInt {
};
if (version) {
this.headers["x-api-version"] = version.toString();
this.headers["x-api-version"] = version.toString() || defaultApiVersion;
}
if (orgConfig) {

View File

@@ -6,8 +6,6 @@ import { CusService } from "@/internal/customers/CusService.js";
import { unsetOrgStripeKeys } from "@/internal/orgs/orgUtils.js";
import type { ExtendedRequest } from "@/utils/models/Request.js";
import type { AutumnContext } from "../../honoUtils/HonoEnv.js";
import { deleteCachedApiCustomer } from "../../internal/customers/cusUtils/apiCusCacheUtils/deleteCachedApiCustomer.js";
import { setCachedApiCusProducts } from "../../internal/customers/cusUtils/apiCusCacheUtils/setCachedApiCusProducts.js";
import type { Logger } from "../logtail/logtailUtils.js";
import { handleCheckoutSessionCompleted } from "./webhookHandlers/handleCheckoutCompleted.js";
import { handleCusDiscountDeleted } from "./webhookHandlers/handleCusDiscountDeleted.js";
@@ -82,32 +80,32 @@ const handleStripeWebhookRefresh = async ({
return;
}
if (updateProductEvents.includes(eventType)) {
const fullCus = await CusService.getFull({
db,
idOrInternalId: cus.id!,
orgId: org.id,
env,
withEntities: true,
withSubs: true,
});
// if (updateProductEvents.includes(eventType)) {
// const fullCus = await CusService.getFull({
// db,
// idOrInternalId: cus.id!,
// orgId: org.id,
// env,
// withEntities: true,
// withSubs: true,
// });
await setCachedApiCusProducts({
ctx,
fullCus,
customerId: cus.id!,
});
} else {
await deleteCachedApiCustomer({
customerId: cus.id!,
orgId: org.id,
env,
source: {
stripeWebhook: true,
eventType,
},
});
}
// await setCachedApiCusProducts({
// ctx,
// fullCus,
// customerId: cus.id!,
// });
// } else {
// await deleteCachedApiCustomer({
// customerId: cus.id!,
// orgId: org.id,
// env,
// source: {
// stripeWebhook: true,
// eventType,
// },
// });
// }
}
};

View File

@@ -125,7 +125,10 @@ export const handleTrack = createRoute({
},
});
if (error) code = "insufficient_balance";
if (error) {
console.log(`Redis deduction error: ${error}`);
code = "insufficient_balance";
}
});
if (!success) {

View File

@@ -79,6 +79,7 @@ export const runRedisDeduction = async ({
}
// Redis deduction successful: queue sync jobs and event insertion
if (result.success) {
for (const deduction of featureDeductions) {
globalSyncBatchingManager.addSyncPair({

View File

@@ -96,7 +96,9 @@ export class SyncBatchingManager {
*/
private async executeCustomerBatch({
customerId,
}: { customerId: string }): Promise<void> {
}: {
customerId: string;
}): Promise<void> {
const customerBatch = this.customerBatches.get(customerId);
if (!customerBatch) return;
@@ -167,7 +169,9 @@ export class SyncBatchingManager {
async flush(): Promise<void> {
const customerIds = Array.from(this.customerBatches.keys());
await Promise.all(
customerIds.map((customerId) => this.executeCustomerBatch({ customerId })),
customerIds.map((customerId) =>
this.executeCustomerBatch({ customerId }),
),
);
}
}

View File

@@ -1,4 +1,8 @@
import { type Feature, FeatureNotFoundError } from "@autumn/shared";
import {
type Feature,
FeatureNotFoundError,
RecaseError,
} from "@autumn/shared";
import type { AutumnContext } from "../../../../honoUtils/HonoEnv.js";
import { getCreditSystemsFromFeature } from "../../../features/creditSystemUtils.js";
@@ -80,5 +84,11 @@ export const getTrackEventNameDeductions = ({
}),
);
if (featureDeductions.length === 0) {
throw new RecaseError({
message: `No features found for event name: ${eventName}`,
});
}
return featureDeductions;
};

View File

@@ -1,7 +1,7 @@
import {
type CreateCustomerParams,
CusExpand,
CusProductStatus,
type CustomerData,
type Entity,
type EntityData,
type FullCustomer,
@@ -33,7 +33,7 @@ export const getOrCreateCustomer = async ({
}: {
req: ExtendedRequest;
customerId: string | null;
customerData?: CustomerData;
customerData?: CreateCustomerParams;
inStatuses?: CusProductStatus[];
skipGet?: boolean;
withEntities?: boolean;
@@ -89,7 +89,9 @@ export const getOrCreateCustomer = async ({
fingerprint: customerData?.fingerprint,
metadata: customerData?.metadata || {},
stripe_id: customerData?.stripe_id,
default_product_id: customerData?.default_product_id,
},
createDefaultProducts: customerData?.disable_default !== true,
})) as FullCustomer;
customer = await CusService.getFull({

View File

@@ -0,0 +1,95 @@
import { AppEnv, type Organization } from "@autumn/shared";
import type { Logger } from "@/external/logtail/logtailUtils.js";
import {
deauthorizeAccount,
deleteConnectedAccount,
} from "@/external/connect/connectUtils.js";
import { deleteSvixApp } from "@/external/svix/svixHelpers.js";
import { deleteStripeWebhook } from "../orgUtils.js";
export const deleteSvixWebhooks = async ({
org,
logger,
}: {
org: Organization;
logger: Logger;
}) => {
const batch = [];
if (org.svix_config?.sandbox_app_id) {
batch.push(
deleteSvixApp({
appId: org.svix_config.sandbox_app_id,
}),
);
}
if (org.svix_config?.live_app_id) {
batch.push(
deleteSvixApp({
appId: org.svix_config.live_app_id,
}),
);
}
try {
await Promise.all(batch);
} catch (error) {
logger.error(`Failed to delete svix webhooks for ${org.id}, ${org.slug}`);
}
};
export const deleteStripeWebhooks = async ({
org,
logger,
}: {
org: Organization;
logger: Logger;
}) => {
try {
await deleteStripeWebhook({
org: org,
env: AppEnv.Sandbox,
});
await deleteStripeWebhook({
org: org,
env: AppEnv.Live,
});
} catch (error: any) {
logger.error(
`Failed to delete stripe webhooks for ${org.id}, ${org.slug}. ${error.message})`,
);
}
};
export const deleteStripeAccounts = async ({
org,
logger,
}: {
org: Organization;
logger: Logger;
}) => {
if (org.test_stripe_connect?.account_id) {
await deauthorizeAccount({
accountId: org.test_stripe_connect.account_id,
env: AppEnv.Sandbox,
logger,
});
}
if (org.live_stripe_connect?.account_id) {
await deauthorizeAccount({
accountId: org.live_stripe_connect.account_id,
env: AppEnv.Live,
logger,
});
}
if (org.test_stripe_connect?.default_account_id) {
await deleteConnectedAccount({
accountId: org.test_stripe_connect.default_account_id,
env: AppEnv.Sandbox,
logger,
});
}
};

View File

@@ -163,6 +163,7 @@ export const handleCreatePlatformOrg = createRoute({
return c.json({
test_secret_key,
live_secret_key,
org_slug: org.slug,
});
},
});

View File

@@ -0,0 +1,96 @@
import {
AppEnv,
customers,
ErrCode,
RecaseError,
organizations,
member,
} from "@autumn/shared";
import { and, eq } from "drizzle-orm";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod/v4";
import type { Context } from "hono";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { OrgService } from "@/internal/orgs/OrgService.js";
import {
deleteStripeAccounts,
deleteSvixWebhooks,
deleteStripeWebhooks,
} from "@/internal/orgs/orgUtils/deleteOrgUtils.js";
const deleteOrgSchema = z.object({
slug: z.string().min(1, "Organization slug is required"),
});
/**
* DELETE /organizations
* Deletes a platform organization by slug (for test cleanup)
*/
export const handleDeletePlatformOrg = [
zValidator("json", deleteOrgSchema),
async (c: Context<HonoEnv>) => {
const ctx = c.get("ctx");
const { db, logger, org: masterOrg } = ctx;
const { slug } = c.req.valid("json");
// Platform API creates orgs with format: {slug}|{masterOrgId}
// So we need to find the org with this pattern
const fullSlug = `${slug}|${masterOrg.id}`;
const org = await OrgService.getBySlug({ db, slug: fullSlug });
if (!org) {
throw new RecaseError({
message: `Organization with slug "${slug}" not found`,
code: ErrCode.NotFound,
statusCode: 404,
});
}
// Check if any live customers exist
const hasCustomers = await db.query.customers.findFirst({
where: and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Live)),
});
if (hasCustomers) {
throw new RecaseError({
message: "Cannot delete org with production mode customers",
code: ErrCode.OrgHasCustomers,
statusCode: 400,
});
}
// Delete svix webhooks
logger.info("1. Deleting svix webhooks");
await deleteSvixWebhooks({ org, logger });
// Delete stripe webhooks
logger.info("2. Deleting stripe webhooks");
await deleteStripeWebhooks({ org, logger });
// Delete stripe accounts
logger.info("3. Deleting stripe accounts");
await deleteStripeAccounts({ org, logger });
// Delete all sandbox customers
logger.info("4. Deleting sandbox customers");
await db
.delete(customers)
.where(
and(eq(customers.org_id, org.id), eq(customers.env, AppEnv.Sandbox)),
);
// Delete memberships
logger.info("5. Deleting org memberships");
await db.delete(member).where(eq(member.organizationId, org.id));
// Delete the organization itself
logger.info("6. Deleting organization");
await db.delete(organizations).where(eq(organizations.id, org.id));
return c.json({
success: true,
message: `Organization "${slug}" deleted successfully`,
});
},
];

View File

@@ -2,6 +2,7 @@ import { Autumn } from "autumn-js";
import { Hono } from "hono";
import type { HonoEnv } from "@/honoUtils/HonoEnv.js";
import { handleCreatePlatformOrg } from "./handlers/handleCreatePlatformOrg.js";
import { handleDeletePlatformOrg } from "./handlers/handleDeletePlatformOrg.js";
import { handleGetPlatformOAuth } from "./handlers/handleGetPlatformOAuth.js";
import { handleListPlatformOrgs } from "./handlers/handleListPlatformOrgs.js";
import { listPlatformUsers } from "./handlers/handleListPlatformUsers.js";
@@ -86,4 +87,11 @@ platformBetaRouter.post(
platformBetaRouter.get("/users", ...listPlatformUsers);
platformBetaRouter.get("/organizations", ...handleListPlatformOrgs);
/**
* DELETE /organizations
* Deletes a platform organization by slug
*/
platformBetaRouter.delete("/organizations", ...handleDeletePlatformOrg);
export { platformBetaRouter };

View File

@@ -118,11 +118,13 @@ export class ProductService {
orgId,
env,
group,
inIds,
}: {
db: DrizzleCli;
orgId: string;
env: AppEnv;
group?: string;
inIds?: string[];
}) {
const prods = (await db.query.products.findMany({
where: and(
@@ -131,6 +133,7 @@ export class ProductService {
eq(products.is_default, true),
ne(products.archived, true),
group ? eq(products.group, group) : undefined,
inIds ? inArray(products.id, inIds) : undefined,
),
with: {
entitlements: {

View File

@@ -0,0 +1,89 @@
import type {
type CreateFreeTrial,
Entitlement,
Feature,
Price,
ProductV2,
} from "@autumn/shared";
import { itemToPriceAndEnt } from "@/internal/products/product-items/productItemUtils/itemToPriceAndEnt.js";
/**
* V1 product format with entitlements as a record (used in tests)
* Different from FullProduct which has entitlements as an array
*/
export type ProductV1 = {
id: string;
name: string;
is_default: boolean;
is_add_on: boolean;
entitlements: Record<string, Entitlement>;
prices: Price[];
free_trial: CreateFreeTrial | null;
group: string;
};
/**
* Converts ProductV2 (items-based) to V1 format (entitlements + prices)
*
* Uses production conversion utilities to ensure test expectations match actual behavior.
*
* @param productV2 - V2 product with items array
* @param orgId - Organization ID
* @param features - Available features in the org
* @returns V1-format product object with entitlements and prices
*/
export const convertProductV2ToV1 = ({
productV2,
orgId,
features,
}: {
productV2: ProductV2;
orgId: string;
features: Feature[];
}): ProductV1 => {
const entitlements: Entitlement[] = [];
const prices: Price[] = [];
for (const item of productV2.items) {
const feature = features.find((f) => f.id === item.feature_id);
// Use production conversion utilities
const { newEnt, newPrice, sameEnt, samePrice } = itemToPriceAndEnt({
item,
orgId,
internalProductId: "test",
feature,
isCustom: false,
features,
});
const ent = newEnt || sameEnt;
const price = newPrice || samePrice;
if (ent) {
entitlements.push(ent);
}
if (price) {
prices.push(price);
}
}
// Convert entitlements array to record keyed by feature_id
const entitlementsRecord: Record<string, Entitlement> = {};
for (const ent of entitlements) {
if (ent.feature_id) {
entitlementsRecord[ent.feature_id] = ent;
}
}
return {
id: productV2.id,
name: productV2.name,
is_default: productV2.is_default,
is_add_on: productV2.is_add_on,
entitlements: entitlementsRecord,
prices,
free_trial: productV2.free_trial,
group: productV2.group,
};
};

View File

@@ -150,6 +150,7 @@ export const constructArrearItem = ({
export const constructArrearProratedItem = ({
featureId,
featureType,
pricePerUnit = 10,
includedUsage = 1,
config = {
@@ -160,6 +161,7 @@ export const constructArrearProratedItem = ({
rolloverConfig,
}: {
featureId: string;
featureType?: ProductItemFeatureType;
pricePerUnit?: number;
includedUsage?: number;
config?: ProductItemConfig;
@@ -178,6 +180,7 @@ export const constructArrearProratedItem = ({
...(rolloverConfig ? { rollover: rolloverConfig } : {}),
},
usage_limit: usageLimit,
...(featureType ? { feature_type: featureType } : {}),
};
return item;

View File

@@ -6,6 +6,7 @@ import {
CreateFreeTrialSchema,
type CreateReward,
FeatureUsageType,
type FreeTrial,
FreeTrialDuration,
type ProductItem,
type ProductV2,
@@ -143,6 +144,16 @@ export const constructProduct = ({
id ||
(isAnnual ? `${type}-annual` : interval ? `${type}-${interval}` : type);
let free_trial: CreateFreeTrial | null = null;
if (freeTrial) {
free_trial = freeTrial as FreeTrial;
} else if (trial) {
free_trial = CreateFreeTrialSchema.parse({
length: 7,
duration: FreeTrialDuration.Day,
});
}
const product: ProductV2 = {
id: id_,
name: id
@@ -158,15 +169,7 @@ export const constructProduct = ({
is_default: (type === "free" && isDefault) || forcePaidDefault,
version: 1,
group: group || "",
free_trial:
freeTrial || trial
? (CreateFreeTrialSchema.parse({
length: 7,
duration: FreeTrialDuration.Day,
unique_fingerprint: false,
card_required: true,
}) as any)
: null,
free_trial: free_trial as FreeTrial,
created_at: Date.now(),
};

View File

@@ -0,0 +1,72 @@
import {
ApiVersion,
customerProducts,
customers,
type ProductV2,
} from "@autumn/shared";
import { and, eq, inArray } from "drizzle-orm";
import { createProducts } from "tests/utils/productUtils.js";
import type { TestContext } from "tests/utils/testInitUtils/createTestContext.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
export const createSharedProducts = async ({
products,
ctx,
}: {
products: ProductV2[];
ctx: TestContext;
}) => {
const { db } = ctx;
let cusProducts = await ctx.db.query.customerProducts.findMany({
where: inArray(
customerProducts.product_id,
products.map((p) => p.id),
),
with: {
product: true,
},
});
cusProducts = cusProducts.filter((cp) => cp.product.org_id === ctx.org.id);
if (cusProducts.length > 10) {
throw new Error("Too many customers under shared default free product");
}
await ctx.db.delete(customers).where(
and(
inArray(
customers.internal_id,
cusProducts.map((cp) => cp.internal_customer_id),
),
eq(customers.env, ctx.env),
eq(customers.org_id, ctx.org.id),
),
);
const autumn = new AutumnInt({
secretKey: ctx.orgSecretKey,
version: ApiVersion.V1_2,
});
try {
await createProducts({
db,
orgId: ctx.org.id,
env: ctx.env,
autumn,
products,
});
} catch (error) {
console.error(
"[createSharedProducts] Failed to create shared products:",
error,
);
console.error(
"Product IDs:",
products.map((p) => p.id),
);
throw error;
}
};

View File

@@ -12,18 +12,25 @@ export const initCustomerV3 = async ({
customerData,
attachPm,
withTestClock = true,
withDefault = false,
defaultProductId,
}: {
ctx: TestContext;
customerId: string;
attachPm?: "success" | "fail";
customerData?: CustomerData;
withTestClock?: boolean;
withDefault?: boolean;
defaultProductId?: string;
}) => {
const name = customerId;
const email = `${customerId}@example.com`;
const fingerprint_ = "";
const { stripeCli } = ctx;
const autumn = new AutumnInt({ version: ApiVersion.V1_2 });
const autumn = new AutumnInt({
version: ApiVersion.V1_2,
secretKey: ctx.orgSecretKey,
});
let testClockId: string | undefined;
@@ -53,6 +60,8 @@ export const initCustomerV3 = async ({
// @ts-expect-error
fingerprint: customerData?.fingerprint || fingerprint_,
stripe_id: stripeCus.id,
disable_default: !withDefault,
default_product_id: defaultProductId,
});
// 3. Attach payment method

View File

@@ -8,21 +8,47 @@ export const initProductsV0 = async ({
ctx,
products,
prefix,
skipPrefixIds = [],
customerId,
customerIds,
}: {
ctx: TestContext;
products: ProductV2[];
prefix?: string;
skipPrefixIds?: string[];
customerId?: string;
customerIds?: string[];
}) => {
// 1. Add prefix to products
// 1. Add prefix to products (except those in skipPrefixIds)
if (prefix) {
const productsToPrefix = products.filter(
(p) => !skipPrefixIds.includes(p.id),
);
addPrefixToProducts({
products,
products: productsToPrefix,
prefix,
});
}
// 2. Create
const autumn = new AutumnInt({ version: ApiVersion.V1_2 });
// 2. Create products using the org's secret key
const autumn = new AutumnInt({
version: ApiVersion.V1_2,
secretKey: ctx.orgSecretKey,
});
if (customerIds) {
for (const id of customerIds) {
try {
await autumn.customers.delete(id);
} catch {}
}
}
if (customerId) {
try {
await autumn.customers.delete(customerId);
} catch {}
}
await createProducts({
db: ctx.db,
orgId: ctx.org.id,

View File

@@ -0,0 +1,354 @@
# Test Migration Guide
## Overview
This guide explains how to migrate test files from the global state pattern to the isolated test context pattern.
## Quick Start Migration Prompt (Copy & Paste)
Use this prompt for AI coding agents to migrate test files:
```
Migrate test file [FILE_PATH] from global state to isolated test context.
**Setup:**
1. Create backup: `cp [FILE_PATH] [FILE_PATH.backup.test.ts]` (DO NOT delete backup)
2. Read migration guide: @server/tests/MIGRATION_GUIDE.md
3. Read original file to understand all test logic
**Critical Rules:**
- PRESERVE ALL test logic, assertions, and edge cases
- DO NOT remove force_checkout tests or any existing tests
- Replace `compareMainProduct` with `expectCustomerV0Correct`
- Use TestFeature enum (Messages, Dashboard, Admin) instead of global features
- Products MUST be created with `initProductsV0` BEFORE customer creation
- Free trials use this exact structure:
```typescript
freeTrial: {
length: 7,
duration: FreeTrialDuration.Day, // Import from @autumn/shared
unique_fingerprint: true,
card_required: true,
}
```
**Migration Steps:**
1. Replace global imports with TestFeature enum
2. Define products inline using `constructProduct()` and feature item constructors
3. Set `const customerId = testCase;` at top of describe block
4. Create AutumnInt instance with `ctx.orgSecretKey` and `ApiVersion.V1_2`
5. In beforeAll:
- Call `initProductsV0({ ctx, products, prefix: testCase, customerId })` FIRST
- Then call `initCustomerV3({ ctx, customerId, ... })`
6. Replace `compareMainProduct` with `expectCustomerV0Correct`
7. For entitlement types, use: `ApiCustomerV1["entitlements"][number]`
8. When checking entitlements, iterate through REFERENCE product (what you sent), not customer data
**Testing:**
Run: `bun test --timeout 0 [FILE_PATH]`
If you encounter unfamiliar utility functions, STOP and ASK how to handle them.
```
## Detailed Migration Prompt for Coding Agent
```
Migrate the test file [FILE_PATH] from using global state to isolated test context.
Reference the migration guide at @server/tests/MIGRATION_GUIDE.md for the full pattern.
**Critical Requirements:**
1. DO NOT remove any existing test logic - preserve ALL test cases and assertions
2. DO NOT remove any force_checkout tests or other edge case tests
3. Compare line-by-line with the original file to ensure nothing is lost
4. If you encounter unfamiliar utility functions (beyond `compareMainProduct`), STOP and ASK the user how to handle them - do not attempt to migrate them on your own
**Migration Steps:**
1. **Create a Backup Copy**
- BEFORE making any changes, create a copy of the test file for reference
- Example: `cp basic2.test.ts basic2.test.ts.backup`
- This allows you to compare line-by-line during migration to ensure nothing is lost
- Delete the backup file after migration is complete and verified
2. **Replace Global Imports**
- Remove: `import { features, products } from "tests/global.js";`
- Add: `import { TestFeature } from "tests/setup/v2Features.js";`
3. **Create Inline Product Definitions**
- Use `constructProduct()` to define products directly in the test file
- Use `constructFeatureItem()`, `constructPrepaidItem()`, etc. for items
- Reference TestFeature enum instead of global features object
- Add a unique prefix to product IDs (e.g., testCase name)
4. **Update Test Setup**
- Add `const customerId = testCase;` at the top of describe block
- Create AutumnInt instance with org secret key:
```typescript
const autumnV1 = new AutumnInt({
secretKey: ctx.orgSecretKey,
version: ApiVersion.V1_2,
});
```
- In beforeAll:
- Call `initProductsV0({ ctx, products: [...], prefix: testCase, customerId })` BEFORE customer creation
- **IMPORTANT:** Passing `customerId` to `initProductsV0` automatically handles customer cleanup if they exist - you DO NOT need to manually delete the customer first
- Call `initCustomerV3({ ctx, customerId, ... })` AFTER products
5. **Update Test Assertions**
- Replace `compareMainProduct` with `expectCustomerV0Correct` for v0.1 API
- Replace references to `features.metered1` with `TestFeature.Messages`
- Replace references to `features.boolean1` with `TestFeature.Dashboard`
- Use `AutumnCli.getCustomer()` for v0.1 API format (returns `features` object)
- Use `autumnV1.customers.get()` for v1.2 API format (returns `entitlements` array)
**Type Helpers:**
- For entitlement types from v0.1 API, use: `ApiCustomerV1["entitlements"][number]`
- Import from: `import type { ApiCustomerV1 } from "@shared/api/customers/previousVersions/apiCustomerV1.js";`
- Example:
```typescript
const addOnBalance = cusRes.entitlements.find(
(e: ApiCustomerV1["entitlements"][number]) =>
e.feature_id === TestFeature.Messages && e.interval === "lifetime"
);
```
**⚠️ CRITICAL - Entitlement Checking Pattern:**
When testing entitlements with `/check` endpoint, you MUST iterate through the **reference product's entitlements** (what you SENT), NOT the customer's entitlements (what they have).
**WRONG (iterating through customer's entitlements):**
```typescript
const customer = await AutumnCli.getCustomer(customerId);
const entitlements = customer.features; // ❌ WRONG!
for (const featureId of Object.keys(entitlements)) {
const res = await AutumnCli.entitled(customerId, featureId);
// checking against customer data...
}
```
**CORRECT (iterating through reference product's entitlements):**
```typescript
import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js";
// Convert ProductV2 to V1 to get reference entitlements
const proProdV1 = convertProductV2ToV1({
productV2: proProd,
orgId: ctx.org.id,
features: ctx.features,
});
const proEntitlements = proProdV1.entitlements;
// Iterate through reference product's entitlements
for (const entitlement of Object.values(proEntitlements)) {
const res = await AutumnCli.entitled(customerId, entitlement.feature_id);
// Check that the response matches what we SENT...
expect(res.allowed).toBe(true);
if (entitlement.allowance) {
const balance = res.balances.find(b => b.feature_id === entitlement.feature_id);
expect(balance?.balance).toBe(entitlement.allowance);
}
}
```
This pattern ensures you're testing what you SENT against what you GET back from the API.
**⚠️ IMPORTANT - Unfamiliar Utility Functions:**
If you encounter utility functions that you're not sure how to migrate (e.g., `compareMainProduct`, `expectProductCorrect`, `checkEntitlements`, or other custom assertion helpers):
- **DO NOT attempt to migrate or replace them on your own**
- **STOP and ASK the user**: "I found utility function [FUNCTION_NAME] at line [LINE]. How should I handle this in the migration?"
- Wait for explicit instructions on the correct replacement function or pattern
- Common replacements so far:
- `compareMainProduct` → `expectCustomerV0Correct`
- But there may be others that need different handling!
6. **Verify All Logic Preserved**
- Check that every test case from the original file exists
- Check that every assertion is present
- Check that force_checkout tests are included
- Check that edge case tests are not removed
7. **Update Test Case ID**
- Change `testCase = "testname"` to keep original name (not "testname-new")
- Update console.log messages to use correct testCase
8. **Run Tests**
- Verify all tests pass with `bun test --timeout 0 [FILE_PATH]`
- The `--timeout 0` flag disables test timeouts, which is necessary for tests that involve checkout flows and longer async operations
**Example Migration:**
Before:
```typescript
import { features, products } from "tests/global.js";
const testCase = "basic1";
describe("basic1", () => {
const customerId = testCase;
beforeAll(async () => {
await initCustomerV3({ ctx, customerId });
});
test("should have correct entitlements", async () => {
const entitled = await AutumnCli.entitled(customerId, features.metered1.id);
expect(entitled.allowed).toBe(true);
});
});
```
After:
```typescript
import { TestFeature } from "tests/setup/v2Features.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const freeProd = constructProduct({
type: "free",
isDefault: true,
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 5,
interval: ProductItemInterval.Month,
}),
],
});
const testCase = "basic1";
const customerId = testCase;
describe("basic1", () => {
const autumnV1 = new AutumnInt({
secretKey: ctx.orgSecretKey,
version: ApiVersion.V1_2,
});
beforeAll(async () => {
// Passing customerId automatically handles cleanup
await initProductsV0({
ctx,
products: [freeProd],
prefix: testCase,
customerId,
});
await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" },
withTestClock: false,
});
});
test("should have correct entitlements", async () => {
const entitled = await AutumnCli.entitled(customerId, TestFeature.Messages);
expect(entitled.allowed).toBe(true);
});
});
```
**After Migration:**
- Replace the original file (not create a .new.test.ts file)
- Verify tests pass
- Report any issues or edge cases found
```
## Common Patterns
### Product Construction
```typescript
// Free product with feature
const freeProd = constructProduct({
type: "free",
isDefault: true,
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 5,
interval: ProductItemInterval.Month,
}),
],
});
// Pro product (matches global products.pro)
// - Boolean feature (Dashboard)
// - Metered feature (Messages) with 10 allowance
// - Unlimited feature (Admin)
// - Monthly subscription price ($20)
const proProd = constructProduct({
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
],
});
// Pro product with free trial
// IMPORTANT: Free trial structure must use this exact format
const proWithTrial = constructProduct({
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Dashboard,
isBoolean: true,
}),
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructFeatureItem({
featureId: TestFeature.Admin,
unlimited: true,
}),
],
freeTrial: {
length: 7,
duration: FreeTrialDuration.Day, // Import FreeTrialDuration from @autumn/shared
unique_fingerprint: true, // Set to true to prevent duplicate trials per fingerprint
card_required: true,
},
});
// Add-on product
const addOnProd = constructProduct({
type: "paid",
id: "addon",
isAddOn: true,
items: [
constructPrepaidItem({
featureId: TestFeature.Messages,
price: 500, // $5.00
billingUnits: 100,
}),
],
});
```
### Feature Mapping
- `features.metered1``TestFeature.Messages`
- `features.boolean1``TestFeature.Dashboard`
- `features.metered2` → Create new feature if needed
### API Version Differences
- **v0.1 API** (AutumnCli): Returns `{ features: { [featureId]: {...} } }`
- **v1.2 API** (AutumnInt): Returns `{ entitlements: [...] }`
## Why This Migration?
1. **Parallel Test Isolation**: Tests can run in parallel without conflicting
2. **No Global State**: Each test has its own products and data
3. **Test Independence**: Tests don't depend on setup order
4. **Better Debugging**: Each test is self-contained and easier to understand

View File

@@ -0,0 +1,457 @@
# Test Migration Tracker
Track the progress of migrating test files from global state to isolated test context.
## Migration Status
Legend:
- ✅ = Migrated and passing
- 🚧 = In progress
- ⏳ = Not started
- ⚠️ = Needs review
- ❌ = Skipped/Archived
## Test Files to Migrate
### Basic Tests
- [x]`tests/attach/basic/basic1.test.ts` - Migrated
- [x]`tests/attach/basic/basic2.test.ts` - Migrated (renamed from basic4)
- [x]`tests/attach/basic/basic3.test.ts` - Migrated (renamed from basic5)
- [x]`tests/attach/basic/basic6.test.ts` - Migrated
- [x]`tests/attach/basic/basic7.test.ts` - Migrated
- [x]`tests/attach/basic/basic8.test.ts` - Migrated
- [x]`tests/attach/basic/basic9.test.ts` - Migrated
- [x]`tests/attach/basic/basic10.test.ts` - Migrated
### Downgrade Tests
- [x]`tests/attach/downgrade/downgrade5.test.ts` - Migrated (global→isolated with shared products)
- [x]`tests/attach/downgrade/downgrade6.test.ts` - Migrated (global→isolated with shared products)
- [x]`tests/attach/downgrade/downgrade7.test.ts` - Migrated (global→isolated with shared products)
### Multi-Product Tests
- [ ]`tests/attach/multiProduct/multiProduct1.ts`
- [ ]`tests/attach/multiProduct/multiProduct2.ts`
### Other Tests
- [ ]`tests/attach/others/others4.ts`
- [ ]`tests/attach/others/others5.ts`
### Upgrade (Old) Tests
- [ ]`tests/attach/upgradeOld/upgradeOld1.ts`
- [ ]`tests/attach/upgradeOld/upgradeOld2.ts`
- [ ]`tests/attach/upgradeOld/upgradeOld3.ts`
- [ ]`tests/attach/upgradeOld/upgradeOld4.ts`
### Core Tests
- [ ]`tests/core/cancel/cancel5.test.ts`
### Continuous Use Tests
- [ ]`tests/contUse/track/track5.ts`
### Advanced Tests
- [ ]`tests/advanced/coupons/coupon1.ts`
- [ ]`tests/advanced/multiFeature/multiFeature1.ts`
- [ ]`tests/advanced/multiFeature/multiFeature2.ts`
- [ ]`tests/advanced/multiFeature/multiFeature3.ts`
### Archived Tests (Review if needed)
- [ ]`tests/archives/arrear_prorated/arrear_prorated2.ts`
- [ ]`tests/archives/arrear_prorated/arrear_prorated3.ts`
- [ ]`tests/archives/coupon1 copy.ts`
## Utility Files (Don't Migrate)
These are helper files, not tests:
- `tests/utils/compare.ts`
- `tests/utils/advancedUsageUtils.ts`
## Migration Prompt
When ready to migrate a file, use this prompt:
```
Migrate the test file [FILE_PATH] from using global state to isolated test context.
Reference the migration guide at @server/tests/MIGRATION_GUIDE.md for the full pattern.
**Critical Requirements:**
1. DO NOT remove any existing test logic - preserve ALL test cases and assertions
2. DO NOT remove any force_checkout tests or other edge case tests
3. Compare line-by-line with the original file to ensure nothing is lost
4. Replace the original file (not create a .new.test.ts file)
5. Update testCase ID to match original (e.g., "basic2" not "basic2-new")
After migration, run: `bun test [FILE_PATH]` to verify all tests pass.
```
## Recent Progress (2025-10-24)
### Migration Tests
- [x]`tests/attach/migrations/migration1.test.ts` - Mocha→Bun migration
- [x]`tests/attach/migrations/migration2.test.ts` - Mocha→Bun migration
- [x]`tests/attach/migrations/migration3.test.ts` - Mocha→Bun migration
- [x]`tests/attach/migrations/migration4.test.ts` - Mocha→Bun migration
- [x]`tests/attach/migrations/runMigrationTest.ts` - Chai→Bun assertions
### Shared Products Created
- [x]`tests/attach/downgrade/sharedProducts.ts` - Created shared products for downgrade tests
## Final Status (2025-10-24)
### G1.sh Test Suite Status
**All 48 test files verified using Bun test framework:**
- ✅ tests/check/basic (10 files)
- ✅ tests/attach/basic (6 files)
- ✅ tests/attach/upgrade (7 files)
- ✅ tests/attach/downgrade (7 files)
- ✅ tests/attach/free (2 files)
- ✅ tests/attach/addOn (2 files)
- ✅ tests/attach/entities (5 files)
- ✅ tests/attach/checkout (8 files)
### G2.sh Test Suite Status
**All 35 active test files migrated to Bun:**
- ✅ Migrations (5 files)
- ✅ NewVersion (3 files)
- ✅ UpgradeOld (5 files including sharedProducts)
- ✅ Others (8 files, 1 deleted)
- ✅ UpdateEnts (5 files including utility)
- ✅ Prepaid (5 files, 2 commented out)
- ✅ Advanced/check (1 file)
- ✅ Interval/upgrade (3 files)
- ✅ Interval/multiSub (3 files)
- ✅ Interval utility (1 file)
## Progress Summary
- **Total Test Files in g1+g2**: 83
- **Migrated**: 83 (100%)
- **In Progress**: 0 (0%)
- **Remaining**: 0 (0%)
## ✅ G2.sh Migration Complete! (All 35 files migrated)
### Migration Tests (5 files)
- [x]`tests/attach/migrations/migration1.test.ts` - Mocha→Bun
- [x]`tests/attach/migrations/migration2.test.ts` - Mocha→Bun
- [x]`tests/attach/migrations/migration3.test.ts` - Mocha→Bun
- [x]`tests/attach/migrations/migration4.test.ts` - Mocha→Bun
- [x]`tests/attach/migrations/runMigrationTest.ts` - Utility (Chai→Bun)
### NewVersion Tests (3 files)
- [x]`tests/attach/newVersion/newVersion1.test.ts` - Mocha→Bun + global→isolated
- [x]`tests/attach/newVersion/newVersion2.test.ts` - Mocha→Bun
- [x]`tests/attach/newVersion/newVersion3.test.ts` - Already migrated
### UpgradeOld Tests (5 files)
- [x]`tests/attach/upgradeOld/upgradeOld1.test.ts` - Mocha→Bun + global→isolated
- [x]`tests/attach/upgradeOld/upgradeOld2.test.ts` - Mocha→Bun + global→isolated
- [x]`tests/attach/upgradeOld/upgradeOld3.test.ts` - Mocha→Bun + global→isolated
- [x]`tests/attach/upgradeOld/upgradeOld4.test.ts` - Mocha→Bun + global→isolated
- [x]`tests/attach/upgradeOld/sharedProducts.ts` - Created for global→isolated migration
### Others Tests (9 files)
- [x]`tests/attach/others/others1.test.ts` - Mocha→Bun
- [x]`tests/attach/others/others2.test.ts` - Mocha→Bun
- [x]`tests/attach/others/others3.test.ts` - Mocha→Bun
- [x]`tests/attach/others/others4.ts` - Deleted (was commented out)
- [x]`tests/attach/others/others5.test.ts` - Mocha→Bun
- [x]`tests/attach/others/others6.test.ts` - Mocha→Bun
- [x]`tests/attach/others/others7.test.ts` - Mocha→Bun
- [x]`tests/attach/others/others8.test.ts` - Mocha→Bun
- [x]`tests/attach/others/others9.test.ts` - Mocha→Bun
### UpdateEnts Tests (5 files)
- [x]`tests/attach/updateEnts/updateEnts1.test.ts` - Mocha→Bun
- [x]`tests/attach/updateEnts/updateEnts2.test.ts` - Mocha→Bun
- [x]`tests/attach/updateEnts/updateEnts3.test.ts` - Mocha→Bun
- [x]`tests/attach/updateEnts/updateEnts4.test.ts` - Mocha→Bun
- [x]`tests/attach/updateEnts/expectUpdateEnts.ts` - Utility (Chai→Bun)
### Prepaid Tests (7 files)
- [x]`tests/attach/prepaid/prepaid1.test.ts` - Mocha→Bun
- [x]`tests/attach/prepaid/prepaid2.test.ts` - Mocha→Bun
- [x]`tests/attach/prepaid/prepaid3.test.ts` - Mocha→Bun
- [x]`tests/attach/prepaid/prepaid4.test.ts` - Mocha→Bun
- [x]`tests/attach/prepaid/prepaid5.test.ts` - Mocha→Bun
- [x] 🔕 `tests/attach/prepaid/prepaid6.ts` - Commented out (not migrated)
- [x] 🔕 `tests/attach/prepaid/prepaid7.ts` - Commented out (not migrated)
### Advanced Tests (1 file)
- [x]`tests/advanced/check/check1.test.ts` - Mocha→Bun
### Interval Tests (7 files)
- [x]`tests/interval/upgrade/interval1.test.ts` - Mocha→Bun
- [x]`tests/interval/upgrade/interval2.test.ts` - Mocha→Bun
- [x]`tests/interval/upgrade/interval3.test.ts` - Mocha→Bun
- [x]`tests/interval/multiSub/multiSubInterval1.test.ts` - Mocha→Bun
- [x]`tests/interval/multiSub/multiSubInterval2.test.ts` - Mocha→Bun
- [x]`tests/interval/multiSub/multiSubInterval3.test.ts` - Mocha→Bun
- [x]`tests/interval/intervalUtils1.test.ts` - Mocha→Bun
## G3 Migration Complete! (All 19 files)
### contUse/entities (5 files)
- [x]`tests/contUse/entities/entity1.test.ts` - Mocha→Bun
- [x]`tests/contUse/entities/entity2.test.ts` - Mocha→Bun
- [x]`tests/contUse/entities/entity3.test.ts` - Mocha→Bun
- [x]`tests/contUse/entities/entity4.test.ts` - Mocha→Bun
- [x]`tests/contUse/entities/entity5.test.ts` - Mocha→Bun
### contUse/update (5 files)
- [x]`tests/contUse/update/updateContUse1.test.ts` - Mocha→Bun
- [x]`tests/contUse/update/updateContUse2.test.ts` - Mocha→Bun
- [x]`tests/contUse/update/updateContUse3.test.ts` - Mocha→Bun
- [x]`tests/contUse/update/updateContUse4.test.ts` - Mocha→Bun
- [x]`tests/contUse/update/updateContUse5.test.ts` - Mocha→Bun
### contUse/track (6 files)
- [x]`tests/contUse/track/track1.test.ts` - Mocha→Bun
- [x]`tests/contUse/track/track2.test.ts` - Mocha→Bun
- [x]`tests/contUse/track/track3.test.ts` - Mocha→Bun
- [x]`tests/contUse/track/track4.test.ts` - Mocha→Bun
- [x]`tests/contUse/track/track5.test.ts` - Mocha→Bun
- [x]`tests/contUse/track/track6.test.ts` - Mocha→Bun
### contUse/roles (3 files)
- [x]`tests/contUse/roles/role1.test.ts` - Mocha→Bun
- [x]`tests/contUse/roles/role2.test.ts` - Mocha→Bun
- [x]`tests/contUse/roles/role3.test.ts` - Mocha→Bun
## G4 Migration Complete! (All 47 files)
### merged/downgrade (8 files)
- [x]`tests/merged/downgrade/mergedDowngrade1.test.ts` - Mocha→Bun
- [x]`tests/merged/downgrade/mergedDowngrade2.test.ts` - Mocha→Bun
- [x]`tests/merged/downgrade/mergedDowngrade3.test.ts` - Mocha→Bun
- [x]`tests/merged/downgrade/mergedDowngrade4.test.ts` - Mocha→Bun
- [x]`tests/merged/downgrade/mergedDowngrade5.test.ts` - Already migrated
- [x]`tests/merged/downgrade/mergedDowngrade6.test.ts` - Already migrated
- [x]`tests/merged/downgrade/mergedDowngrade8.test.ts` - Mocha→Bun
- [x]`tests/merged/downgrade/mergedDowngrade9.test.ts` - Mocha→Bun
### merged/prepaid (3 files)
- [x]`tests/merged/prepaid/mergedPrepaid1.test.ts` - Mocha→Bun
- [x]`tests/merged/prepaid/mergedPrepaid2.test.ts` - Mocha→Bun
- [x]`tests/merged/prepaid/mergedPrepaid3.test.ts` - Mocha→Bun
### Other merged/core directories (36 files - all already migrated)
- [x] ✅ merged/group (2 files)
- [x] ✅ merged/add (3 files)
- [x] ✅ merged/separate (2 files)
- [x] ✅ merged/upgrade (4 files)
- [x] ✅ merged/trial (8 files)
- [x] ✅ merged/addOn (6 files)
- [x] ✅ core/cancel (8 files)
- [x] ✅ core/multiAttach (6 files + subdirectories)
- [x] ✅ core/reset (1 file)
### Utility Files Updated:
- [x]`tests/merged/mergeUtils/expectSubCorrect.ts` - Chai→Bun assertions (kept as .ts)
## G5 Migration Complete! (34 files migrated, but only 19 in g5.sh script)
### multiProduct (2 files + sharedProducts)
- [x]`tests/attach/multiProduct/multiProduct1.test.ts` - Mocha→Bun + global→isolated
- [x]`tests/attach/multiProduct/multiProduct2.test.ts` - Mocha→Bun + global→isolated
- [x]`tests/attach/multiProduct/sharedProducts.ts` - Created
### usage (4 files + sharedProducts)
- [x]`tests/advanced/usage/usage1.test.ts` - Mocha→Bun + global→isolated
- [x]`tests/advanced/usage/usage2.test.ts` - Mocha→Bun (GPU products still use global)
- [x]`tests/advanced/usage/usage3.test.ts` - Mocha→Bun (GPU products still use global)
- [x]`tests/advanced/usage/usage4.test.ts` - Mocha→Bun (GPU products still use global)
- [x]`tests/advanced/usage/sharedProducts.ts` - Created
### coupons (3 files)
- [x]`tests/advanced/coupons/coupon1.test.ts` - Mocha→Bun
- [x]`tests/advanced/coupons/coupon2.test.ts` - Mocha→Bun
- [x]`tests/advanced/coupons/coupon3.test.ts` - Mocha→Bun
### referrals (4 files)
- [x]`tests/advanced/referrals/referrals1.test.ts` - Mocha→Bun
- [x]`tests/advanced/referrals/referrals2.test.ts` - Mocha→Bun
- [x]`tests/advanced/referrals/referrals3.test.ts` - Mocha→Bun
- [x]`tests/advanced/referrals/referrals4.test.ts` - Mocha→Bun
### referrals/paid (4 files)
- [x]`tests/advanced/referrals/paid/referrals13.test.ts` - Mocha→Bun
- [x]`tests/advanced/referrals/paid/referrals14.test.ts` - Mocha→Bun
- [x]`tests/advanced/referrals/paid/referrals15.test.ts` - Mocha→Bun
- [x] 🔕 `tests/advanced/referrals/paid/referrals16.test.ts` - Commented out
### updateQuantity (1 file)
- [x]`tests/attach/updateQuantity/updateQuantity1.test.ts` - Mocha→Bun
### rollovers (6 files) ⚠️ NOT IN g5.sh SCRIPT
- [x]`tests/advanced/rollovers/rollover1.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x]`tests/advanced/rollovers/rollover2.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x]`tests/advanced/rollovers/rollover3.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x]`tests/advanced/rollovers/rollover4.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x]`tests/advanced/rollovers/rollover5.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x]`tests/advanced/rollovers/rollover6.test.ts` - Mocha→Bun (migrated but not in g5.sh)
### customInterval (5 files) ⚠️ NOT IN g5.sh SCRIPT
- [x]`tests/advanced/customInterval/customInterval1.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x]`tests/advanced/customInterval/customInterval2.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x]`tests/advanced/customInterval/customInterval3.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x]`tests/advanced/customInterval/customInterval4.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x]`tests/advanced/customInterval/customInterval5.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x] 🔕 `tests/advanced/customInterval/customInterval6.ts` - Empty file (skipped)
### usageLimit (4 files) ⚠️ NOT IN g5.sh SCRIPT
- [x]`tests/advanced/usageLimit/usageLimit1.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x]`tests/advanced/usageLimit/usageLimit2.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x]`tests/advanced/usageLimit/usageLimit3.test.ts` - Mocha→Bun (migrated but not in g5.sh)
- [x]`tests/advanced/usageLimit/usageLimit4.test.ts` - Mocha→Bun (migrated but not in g5.sh)
### G5 Not Migrated (not in g5.sh script):
- [ ] ⏸️ `tests/advanced/multiFeature/multiFeature1.ts` (uses old ProductV1 structure)
- [ ] ⏸️ `tests/advanced/multiFeature/multiFeature2.ts` (uses old ProductV1 structure)
- [ ] ⏸️ `tests/advanced/multiFeature/multiFeature3.ts` (uses old ProductV1 structure)
**⚠️ ACTION REQUIRED:** The g5.sh comment says "rollovers, customInterval, usageLimit still use Mocha (not migrated yet)" but these 15 files ARE migrated. Either:
1. Add these directories to g5.sh script, OR
2. Create a new test group (g7.sh) for these migrated advanced tests
## G6 - Alex Tests (⏳ NOT MIGRATED - Still Using Mocha)
### Alex Integration Tests (6 test files)
- [ ]`tests/alex/01_free.ts` - Uses Mocha (not migrated)
- [ ]`tests/alex/02_pro.ts` - Uses Mocha (not migrated)
- [ ]`tests/alex/03_premium.ts` - Uses Mocha (not migrated)
- [ ]`tests/alex/04_topups.ts` - Uses Mocha (not migrated)
- [ ]`tests/alex/05_cancel.ts` - Uses Mocha (not migrated)
- [ ]`tests/alex/06_switch.ts` - Uses Mocha (not migrated)
### Utility Files (3 files)
- `tests/alex/00_setup.ts` - Setup file (ignored in g6.sh)
- `tests/alex/utils.ts` - Helper utilities
- `tests/alex/init.ts` - Initialization utilities
**Note:** g6.sh runs these tests using `npx mocha --parallel` with comment "will be migrated later"
## Final Migration Summary
### Totals:
- **G1:** 47 files ✅
- **G2:** 35 files ✅ (includes 7 interval tests)
- **G3:** 19 files ✅
- **G4:** 65 files ✅ (all merged/core tests)
- **G5:** 19 files in script ✅ + 15 files migrated but not in script ⚠️
- **G6:** 6 files ⏳ (NOT migrated - still using Mocha)
- **Total Migrated to Bun:** 219 files (204 in scripts + 15 orphaned)
- **Total in Test Scripts (g1-g5):** 185 files
- **Not migrated:**
- 3 files (multiFeature 1-3 - ProductV1 structure) ⏸️
- 6 files (alex tests - still using Mocha) ⏳
- 15 files (rollovers, customInterval, usageLimit - migrated but not in g5.sh) ⚠️
### Helper Functions Created/Updated:
1.`checkUsageInvoiceAmountV2` - V2 wrapper for usage invoice validation
2.`expectSubCorrect.ts` - Updated Chai→Bun assertions
### Shared Products Files Created:
1.`tests/attach/basic/sharedProducts.ts` (pre-existing)
2.`tests/attach/downgrade/sharedProducts.ts`
3.`tests/attach/upgradeOld/sharedProducts.ts`
4.`tests/attach/multiProduct/sharedProducts.ts`
5.`tests/advanced/usage/sharedProducts.ts`
### Shell Scripts Status:
-`scripts/testGroups/g1.sh` - Uses `BUN_PARALLEL_COMPACT` (47 files)
-`scripts/testGroups/g2.sh` - Uses `BUN_PARALLEL_COMPACT` (35 files, includes interval tests)
-`scripts/testGroups/g3.sh` - Uses `BUN_PARALLEL_COMPACT` (19 files)
-`scripts/testGroups/g4.sh` - Uses `BUN_PARALLEL_COMPACT` (65 files)
- ⚠️ `scripts/testGroups/g5.sh` - Uses `BUN_PARALLEL_COMPACT` (19 files)
- **MISSING:** rollovers (6), customInterval (5), usageLimit (4) directories
- Comment says these "still use Mocha" but they ARE migrated
-`scripts/testGroups/g6.sh` - Uses `npx mocha --parallel` (6 files, not migrated)
### All before() → beforeAll() Replaced:
- ✅ Verified: 0 test files still using `before()` (all occurrences replaced with `beforeAll()`)
- ✅ All test files now use proper Bun test syntax
### Cleanup Actions Completed:
- ✅ Deleted 15 Mocha duplicate .ts files where .test.ts versions existed (coupons, referrals, usage)
- ✅ Renamed 1 Bun duplicate to .backup.ts (updateQuantity1.ts)
- ✅ Created backups for all newly migrated files
### Migration Status:
- ✅ All ProductV1→ProductV2 conversions complete (except 3 multiFeature files + 6 alex files)
- ✅ All Mocha→Bun framework migrations complete (except 3 multiFeature files + 6 alex files)
- ✅ All global state → isolated migrations complete for migrated files
- ✅ All tests preserve original logic and assertions
- ✅ Test groups G1-G4 ready for parallel Bun execution
- ⚠️ G5 is partial - missing 15 migrated test files (rollovers, customInterval, usageLimit)
- ⏳ G6 (alex tests) still uses Mocha framework
---
## 🚨 CRITICAL DISCREPANCIES FOUND
### Issue 1: G2 Missing Interval Tests in Tracker
**Status:** FIXED ✅
- Added 7 interval test files to tracker (interval/upgrade, interval/multiSub)
- Updated G2 count from 28 to 35 files
### Issue 2: G5 - Orphaned Migrated Tests
**Status:** ⚠️ NEEDS ACTION
- **15 test files are migrated but NOT in g5.sh script:**
- `tests/advanced/rollovers/` (6 files)
- `tests/advanced/customInterval/` (5 files)
- `tests/advanced/usageLimit/` (4 files)
- **g5.sh comment is outdated:** Says these "still use Mocha (not migrated yet)" but they ARE migrated
- **Action needed:** Either add these to g5.sh OR create g7.sh for them
### Issue 3: G6 Not Tracked
**Status:** FIXED ✅
- Added G6 section tracking 6 alex test files (still using Mocha)
- These are integration tests that will need migration later
### Issue 4: Incorrect Total Counts
**Status:** FIXED ✅
- Old claim: "204 files migrated"
- **Actual:** 219 files migrated to Bun (but only 185 are in test scripts g1-g5)
- 15 orphaned files exist but aren't run by any script
---
## 📋 RECOMMENDED ACTIONS
1. **Update g5.sh to include orphaned tests:**
```bash
# Add to scripts/testGroups/g5.sh:
BUN_PARALLEL_COMPACT \
'server/tests/advanced/coupons' \
'server/tests/attach/updateQuantity' \
'server/tests/advanced/referrals' \
'server/tests/advanced/referrals/paid' \
'server/tests/attach/multiProduct' \
'server/tests/advanced/usage' \
'server/tests/advanced/rollovers' \
'server/tests/advanced/customInterval' \
'server/tests/advanced/usageLimit' \
--max=6
```
2. **Update g5.sh comment:**
- Remove: "Note: advanced/multiFeature, advanced/rollovers, advanced/customInterval, advanced/usageLimit still use Mocha (not migrated yet)"
- Replace: "Note: advanced/multiFeature still uses Mocha (not migrated yet)"
3. **Consider migrating G6 (alex tests):**
- 6 integration test files still using Mocha
- Would complete the Mocha→Bun migration (except multiFeature)
---
## ✅ VERIFIED COUNTS (Post-Sweep)
- **G1:** 47 files ✅ (matches script)
- **G2:** 35 files ✅ (matches script - corrected from 28)
- **G3:** 19 files ✅ (matches script)
- **G4:** 65 files ✅ (matches script)
- **G5:** 19 files in script, 15 files orphaned ⚠️
- **G6:** 6 files using Mocha ⏳
- **Total in scripts (g1-g5):** 185 files
- **Total migrated to Bun:** 219 files
- **Orphaned (migrated but not in scripts):** 15 files
- **Still using Mocha:** 9 files (3 multiFeature + 6 alex)

View File

@@ -0,0 +1,31 @@
# Test Writing Guide
## Initial Notes (To be organized later)
### Customer Initialization
#### Default Products
- For tests involving default products, use the `withDefault: true` flag in `initCustomerV3()`
- This ensures the customer is created with the default product attached
- Example:
```typescript
await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" },
withTestClock: false,
withDefault: true, // Attach default product on creation
});
```
#### Fingerprint
- For tests involving fingerprint, pass in `fingerprint` through `customerData` in `initCustomerV3()`
- Example:
```typescript
await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" }, // Pass fingerprint here
withTestClock: false,
});
```

View File

@@ -0,0 +1,229 @@
import {
type AppEnv,
type Customer,
LegacyVersion,
type Organization,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import { rewards } from "tests/global.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { timeout } from "tests/utils/genUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import {
advanceTestClock,
completeCheckoutForm,
getDiscount,
} from "tests/utils/stripeUtils.js";
import {
addPrefixToProducts,
getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
const testCase = "coupon1";
const pro = constructProduct({
type: "pro",
items: [constructArrearItem({ featureId: TestFeature.Words })],
});
const simulateOneCycle = async ({
customerId,
db,
org,
env,
stripeCli,
autumn,
testClockId,
couponAmount,
curUnix,
}: {
customerId: string;
db: DrizzleCli;
org: Organization;
env: AppEnv;
stripeCli: Stripe;
autumn: AutumnInt;
testClockId: string;
couponAmount: number;
curUnix: number;
}) => {
const usage = Math.random() * 100000 + 10000;
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: usage,
});
// Expected invoice total
const expectedTotal = await getExpectedInvoiceTotal({
usage: [{ featureId: TestFeature.Words, value: usage }],
customerId,
productId: pro.id,
db,
org,
env,
stripeCli,
});
couponAmount -= expectedTotal;
curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addHours(
addMonths(curUnix, 1),
hoursToFinalizeInvoice,
).getTime(),
waitForSeconds: 30,
});
const customer = await autumn.customers.get(customerId);
expect(customer.invoices![0].total).toBe(0);
const cusDiscount = await getDiscount({
stripeCli: stripeCli,
stripeId: customer.stripe_id!,
});
expect(cusDiscount).toBeDefined();
expect(getOriginalCouponId(cusDiscount.coupon?.id)).toBe(
rewards.rolloverAll.id,
);
expect(cusDiscount.coupon?.amount_off).toBe(
Math.round(couponAmount * 100),
);
return {
couponAmount,
curUnix,
};
};
describe(
chalk.yellow(
`${testCase} - Testing invoice credits reward, apply to all product`,
),
() => {
const customerId = "coupon1";
let stripeCli: Stripe;
let customer: Customer;
let testClockId: string;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let couponAmount = rewards.rolloverAll.discount_config.discount_value;
let curUnix = new Date().getTime();
beforeAll(async () => {
db = ctx.db;
org = ctx.org;
env = ctx.env;
stripeCli = ctx.stripeCli;
const res = await initCustomerV3({
ctx,
customerId,
});
addPrefixToProducts({
products: [pro],
prefix: testCase,
});
await createProducts({
products: [pro],
orgId: org.id,
env,
db,
autumn,
});
testClockId = res.testClockId;
customer = res.customer;
});
// CYCLE 0
test("should attach pro", async () => {
const res = await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
await completeCheckoutForm(
res.checkout_url,
undefined,
rewards.rolloverAll.id,
);
await timeout(10000);
couponAmount -= getBasePrice({ product: pro });
const customer = await autumn.customers.get(customerId);
expectProductAttached({ customer, product: pro });
expect(customer.invoices![0].total).toBe(0);
const cusDiscount = await getDiscount({
stripeCli,
stripeId: customer.stripe_id!,
});
expect(cusDiscount).toBeDefined();
expect(getOriginalCouponId(cusDiscount.coupon?.id)).toBe(
rewards.rolloverAll.id,
);
expect(cusDiscount.coupon?.amount_off).toBe(couponAmount * 100);
});
test("should run one cycle and have correct invoice + coupon amount", async () => {
const res = await simulateOneCycle({
customerId,
db,
org,
env,
stripeCli,
autumn,
testClockId,
couponAmount,
curUnix: new Date().getTime(),
});
couponAmount = res.couponAmount;
curUnix = res.curUnix;
});
// CYCLE 1
test("should run another cycle and have correct invoice + coupon amount", async () => {
const res = await simulateOneCycle({
customerId,
db,
org,
env,
stripeCli,
autumn,
testClockId,
couponAmount,
curUnix,
});
});
},
);

View File

@@ -0,0 +1,192 @@
import {
type AppEnv,
CouponDurationType,
type CreateReward,
LegacyVersion,
type Organization,
RewardType,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import { Decimal } from "decimal.js";
import type Stripe from "stripe";
import { TestFeature } from "tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { timeout } from "tests/utils/genUtils.js";
import { createProducts, createReward } from "tests/utils/productUtils.js";
import { completeCheckoutForm, getDiscount } from "tests/utils/stripeUtils.js";
import {
addPrefixToProducts,
getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { getOriginalCouponId } from "@/internal/rewards/rewardUtils.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
const pro = constructProduct({
type: "pro",
items: [constructArrearItem({ featureId: TestFeature.Words })],
});
const testCase = "coupon2";
// Create reward input
const reward: CreateReward = {
id: "usage",
name: "usage",
promo_codes: [{ code: "usage" }],
type: RewardType.InvoiceCredits,
discount_config: {
discount_value: 10000,
duration_type: CouponDurationType.Forever,
duration_value: 1,
should_rollover: true,
apply_to_all: false,
price_ids: [],
},
};
describe(
chalk.yellow(`${testCase} - Testing one-off rollover, apply to usage only`),
() => {
const customerId = testCase;
let stripeCli: Stripe;
let testClockId: string;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let org: Organization;
let env: AppEnv;
let db: DrizzleCli;
let couponAmount = reward.discount_config?.discount_value ?? 0;
beforeAll(async () => {
org = ctx.org;
env = ctx.env;
db = ctx.db;
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
});
testClockId = testClockId1;
addPrefixToProducts({
products: [pro],
prefix: testCase,
});
await createProducts({
orgId: ctx.org.id,
env: ctx.env,
db: ctx.db,
autumn,
products: [pro],
});
await createReward({
orgId: org.id,
env,
db,
autumn,
reward,
productId: pro.id,
onlyUsage: true,
});
});
// CYCLE 0
test("should attach pro with promo code", async () => {
const res = await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
await completeCheckoutForm(res.checkout_url, undefined, reward.id);
await timeout(10000);
const customer = await autumn.customers.get(customerId);
expectProductAttached({
customer,
product: pro,
});
});
test("should have fixed price invoice and correct remaining coupon amount", async () => {
const customer = await autumn.customers.get(customerId);
const fixedPrice = getBasePrice({ product: pro });
expect(customer.invoices![0].total).toBe(fixedPrice);
const cusDiscount = await getDiscount({
stripeCli,
stripeId: customer.stripe_id!,
});
expect(getOriginalCouponId(cusDiscount.coupon?.id)).toBe(reward.id);
expect(cusDiscount.coupon?.amount_off).toBe(couponAmount * 100);
});
// CYCLE 1
test("should track usage and have correct invoice amount", async () => {
const usage = new Decimal(Math.random() * 1250120 + 10000)
.toDecimalPlaces(2)
.toNumber();
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: usage,
});
const usageTotal = await getExpectedInvoiceTotal({
org,
env,
db,
customerId,
productId: pro.id,
usage: [{ featureId: TestFeature.Words, value: usage }],
stripeCli,
onlyIncludeUsage: true,
});
const basePrice = getBasePrice({ product: pro });
couponAmount = couponAmount - usageTotal;
await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addHours(
addMonths(new Date(), 1),
hoursToFinalizeInvoice,
).getTime(),
waitForSeconds: 20,
});
const customer = await autumn.customers.get(customerId);
expect(customer.invoices![0].total).toBe(basePrice);
const cusDiscount = await getDiscount({
stripeCli,
stripeId: customer.stripe_id!,
});
expect(getOriginalCouponId(cusDiscount.coupon?.id)).toBe(reward.id);
expect(cusDiscount.coupon?.amount_off).toBe(
Math.round(couponAmount * 100),
);
});
},
);

View File

@@ -0,0 +1,171 @@
import {
type AppEnv,
CouponDurationType,
type CreateReward,
LegacyVersion,
type Organization,
RewardType,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import { TestFeature } from "tests/setup/v2Features.js";
import { expectAttachCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { createProducts, createReward } from "tests/utils/productUtils.js";
import {
addPrefixToProducts,
getBasePrice,
} from "tests/utils/testProductUtils/testProductUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import {
constructArrearItem,
constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
const pro = constructProduct({
type: "pro",
items: [constructArrearItem({ featureId: TestFeature.Words })],
});
const oneOff = constructProduct({
type: "one_off",
items: [
constructFeatureItem({
featureId: TestFeature.Words,
includedUsage: 500,
}),
],
});
// Create reward input
const rewardId = "attach_coupon";
const promoCode = "attach_coupon_code";
const reward: CreateReward = {
id: rewardId,
name: "attach_coupon",
promo_codes: [{ code: promoCode }],
type: RewardType.FixedDiscount,
discount_config: {
discount_value: 5,
duration_type: CouponDurationType.OneOff,
duration_value: 1,
should_rollover: true,
apply_to_all: true,
price_ids: [],
},
};
const testCase = "coupon3";
describe(chalk.yellow(`${testCase} - Testing attach coupon`), () => {
const customerId = testCase;
let stripeCli: Stripe;
let testClockId: string;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let org: Organization;
let env: AppEnv;
let db: DrizzleCli;
const couponAmount = reward.discount_config!.discount_value;
beforeAll(async () => {
org = ctx.org;
env = ctx.env;
db = ctx.db;
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
attachPm: "success",
});
testClockId = testClockId1;
addPrefixToProducts({
products: [pro, oneOff],
prefix: testCase,
});
await createProducts({
orgId: ctx.org.id,
env: ctx.env,
db: ctx.db,
autumn,
products: [pro, oneOff],
});
await createReward({
orgId: org.id,
env,
db,
autumn,
reward,
productId: pro.id,
});
});
// CYCLE 0
test("should attach pro with reward ID", async () => {
const res = await autumn.attach({
customer_id: customerId,
product_id: pro.id,
reward: rewardId,
});
const customer = await autumn.customers.get(customerId);
expectAttachCorrect({
customer,
product: pro,
});
const invoice = customer.invoices![0];
const basePrice = getBasePrice({ product: pro });
expect(invoice.total).toBe(basePrice - couponAmount);
});
test("should attach one off with reward ID", async () => {
await autumn.attach({
customer_id: customerId,
product_id: oneOff.id,
reward: rewardId,
});
const customer = await autumn.customers.get(customerId);
expectAttachCorrect({
customer,
product: oneOff,
});
const invoice = customer.invoices![0];
const basePrice = getBasePrice({ product: oneOff });
expect(invoice.total).toBe(basePrice - couponAmount);
expect(invoice.product_ids).toContain(oneOff.id);
});
test("should attach one off with promo code", async () => {
await autumn.attach({
customer_id: customerId,
product_id: oneOff.id,
reward: promoCode,
});
const customer = await autumn.customers.get(customerId);
expectAttachCorrect({
customer,
product: oneOff,
});
expect(customer.invoices!.length).toBe(3);
const basePrice = getBasePrice({ product: oneOff });
for (let i = 0; i < 2; i++) {
const invoice = customer.invoices![i];
expect(invoice.total).toBe(basePrice - couponAmount);
expect(invoice.product_ids).toContain(oneOff.id);
}
});
});

View File

@@ -0,0 +1,145 @@
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { createProducts } from "tests/utils/productUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
const testCase = "customInterval1";
export const pro = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Words,
intervalCount: 2,
includedUsage: 500,
}),
],
intervalCount: 2,
type: "pro",
});
export const premium = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Words,
intervalCount: 2,
}),
// constructArrearItem({ featureId: TestFeature.Words }),
// constructArrearProratedItem({
// featureId: TestFeature.Users,
// pricePerUnit: 30,
// }),
],
intervalCount: 2,
type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing custom interval and interval count`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
addPrefixToProducts({
products: [pro, premium],
prefix: testCase,
});
await createProducts({
autumn,
products: [pro, premium],
db,
orgId: org.id,
env,
});
testClockId = testClockId1!;
});
it("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
});
});
const usage = 100012;
it("should upgrade to premium product and have correct invoice next cycle", async () => {
const curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addMonths(new Date(), 1).getTime(),
waitForSeconds: 15,
});
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli,
db,
org,
env,
});
const customer = await autumn.customers.get(customerId);
expect(customer.invoices.length).to.equal(2);
const nextUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addHours(
addMonths(new Date(curUnix), 1),
hoursToFinalizeInvoice,
).getTime(),
waitForSeconds: 30,
});
const customer2 = await autumn.customers.get(customerId);
const invoices = customer2.invoices;
expect(invoices.length).to.equal(3);
expect(invoices[0].product_ids).to.include(premium.id);
expect(invoices[0].total).to.equal(getBasePrice({ product: premium }));
const wordsFeature = customer2.features[TestFeature.Words];
// @ts-expect-error
expect(wordsFeature.interval_count).to.equal(2);
});
});

View File

@@ -0,0 +1,129 @@
import { LegacyVersion } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
const testCase = "customInterval1";
export const pro = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Words,
intervalCount: 2,
includedUsage: 500,
}),
],
intervalCount: 2,
type: "pro",
});
export const premium = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Words,
intervalCount: 2,
}),
// constructArrearItem({ featureId: TestFeature.Words }),
// constructArrearProratedItem({
// featureId: TestFeature.Users,
// pricePerUnit: 30,
// }),
],
intervalCount: 2,
type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing custom interval and interval count`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let stripeCli: Stripe;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, premium],
prefix: testCase,
customerId,
});
testClockId = testClockId1!;
});
test("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
const usage = 100012;
test("should upgrade to premium product and have correct invoice next cycle", async () => {
const curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addMonths(new Date(), 1).getTime(),
waitForSeconds: 15,
});
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
const customer = await autumn.customers.get(customerId);
expect(customer.invoices.length).toBe(2);
const nextUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addHours(
addMonths(new Date(curUnix), 1),
hoursToFinalizeInvoice,
).getTime(),
waitForSeconds: 30,
});
const customer2 = await autumn.customers.get(customerId);
const invoices = customer2.invoices;
expect(invoices.length).toBe(3);
expect(invoices[0].product_ids).toContain(premium.id);
expect(invoices[0].total).toBe(getBasePrice({ product: premium }));
const wordsFeature = customer2.features[TestFeature.Words];
// @ts-expect-error
expect(wordsFeature.interval_count).toBe(2);
});
});

View File

@@ -0,0 +1,118 @@
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
const testCase = "customInterval2";
export const pro = constructRawProduct({
id: "pro",
items: [
constructArrearItem({
includedUsage: 0,
featureId: TestFeature.Words,
intervalCount: 2,
}),
],
});
describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear prorated price`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
addPrefixToProducts({
products: [pro],
prefix: testCase,
});
await createProducts({
autumn,
products: [pro],
db,
orgId: org.id,
env,
});
testClockId = testClockId1!;
});
it("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
});
});
const usage = 100012;
it("should upgrade to premium product and have correct invoice next cycle", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: usage,
});
const curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addHours(
addMonths(new Date(), 2),
hoursToFinalizeInvoice,
).getTime(),
waitForSeconds: 30,
});
const invoiceAmount = await getExpectedInvoiceTotal({
customerId,
productId: pro.id,
usage: [{ featureId: TestFeature.Words, value: usage }],
stripeCli,
db,
org,
env,
});
const customer = await autumn.customers.get(customerId);
expect(customer.invoices.length).to.equal(2);
expect(invoiceAmount).to.equal(customer.invoices[0].total);
});
});

View File

@@ -0,0 +1,102 @@
import { LegacyVersion } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { getExpectedInvoiceTotal } from "tests/utils/expectUtils/expectInvoiceUtils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearItem } from "@/utils/scriptUtils/constructItem.js";
import { constructRawProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
const testCase = "customInterval2";
export const pro = constructRawProduct({
id: "pro",
items: [
constructArrearItem({
includedUsage: 0,
featureId: TestFeature.Words,
intervalCount: 2,
}),
],
});
describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on arrear prorated price`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let stripeCli: Stripe;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
customerId,
});
testClockId = testClockId1!;
});
test("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
const usage = 100012;
test("should upgrade to premium product and have correct invoice next cycle", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: usage,
});
const curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addHours(
addMonths(new Date(), 2),
hoursToFinalizeInvoice,
).getTime(),
waitForSeconds: 30,
});
const invoiceAmount = await getExpectedInvoiceTotal({
customerId,
productId: pro.id,
usage: [{ featureId: TestFeature.Words, value: usage }],
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
const customer = await autumn.customers.get(customerId);
expect(customer.invoices.length).toBe(2);
expect(invoiceAmount).toBe(customer.invoices[0].total);
});
});

View File

@@ -0,0 +1,160 @@
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import { addDays, addMonths } from "date-fns";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { createProducts } from "tests/utils/productUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import {
constructFeatureItem,
constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import {
constructProduct,
constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
const testCase = "customInterval3";
export const pro = constructProduct({
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Words,
intervalCount: 2,
}),
],
intervalCount: 2,
});
const prepaidWordsItem = constructPrepaidItem({
featureId: TestFeature.Words,
price: 10,
billingUnits: 1,
includedUsage: 0,
intervalCount: 2,
});
export const addOn = constructRawProduct({
id: "addOn",
items: [prepaidWordsItem],
isAddOn: true,
});
describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on add on merged product`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
addPrefixToProducts({
products: [pro, addOn],
prefix: testCase,
});
await createProducts({
autumn,
products: [pro, addOn],
db,
orgId: org.id,
env,
});
testClockId = testClockId1!;
});
it("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
});
});
it("should upgrade to attached add on and have correct invoice next cycle", async () => {
const curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addDays(new Date(), 20).getTime(),
waitForSeconds: 15,
});
const wordBillingSets = 2;
const wordsBillingUnits = prepaidWordsItem.billing_units! * wordBillingSets;
await autumn.attach({
customer_id: customerId,
product_id: addOn.id,
options: [
{
feature_id: TestFeature.Words,
quantity: wordsBillingUnits,
},
],
});
const customer = await autumn.customers.get(customerId);
const proProduct = customer.products.find((p) => p.id === pro.id);
const invoices = customer.invoices;
expectProductAttached({
customer,
product: pro,
});
expectProductAttached({
customer,
product: addOn,
});
const expectedPrice = wordsBillingUnits * prepaidWordsItem.price!;
const proratedPrice = calculateProrationAmount({
amount: expectedPrice,
periodStart: new Date().getTime(),
periodEnd: addMonths(new Date(), 2).getTime(),
now: curUnix!,
});
expect(invoices[0].product_ids).to.include(addOn.id);
expect(invoices[0].total).to.approximately(proratedPrice, 0.1);
const expectedAddonEnd = addMonths(new Date(), 2);
const approximate = 1000 * 60 * 60 * 24; // +- 1 day
const addOnProduct = customer.products.find((p) => p.id === addOn.id);
expect(addOnProduct?.current_period_end).to.be.approximately(
expectedAddonEnd.getTime(),
approximate,
);
});
});

View File

@@ -0,0 +1,144 @@
import { LegacyVersion } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addDays, addMonths } from "date-fns";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import {
constructFeatureItem,
constructPrepaidItem,
} from "@/utils/scriptUtils/constructItem.js";
import {
constructProduct,
constructRawProduct,
} from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
const testCase = "customInterval3";
export const pro = constructProduct({
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Words,
intervalCount: 2,
}),
],
intervalCount: 2,
});
const prepaidWordsItem = constructPrepaidItem({
featureId: TestFeature.Words,
price: 10,
billingUnits: 1,
includedUsage: 0,
intervalCount: 2,
});
export const addOn = constructRawProduct({
id: "addOn",
items: [prepaidWordsItem],
isAddOn: true,
});
describe(`${chalk.yellowBright(`${testCase}: Testing custom interval on add on merged product`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let stripeCli: Stripe;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, addOn],
prefix: testCase,
customerId,
});
testClockId = testClockId1!;
});
test("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
test("should upgrade to attached add on and have correct invoice next cycle", async () => {
const curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addDays(new Date(), 20).getTime(),
waitForSeconds: 15,
});
const wordBillingSets = 2;
const wordsBillingUnits = prepaidWordsItem.billing_units! * wordBillingSets;
await autumn.attach({
customer_id: customerId,
product_id: addOn.id,
options: [
{
feature_id: TestFeature.Words,
quantity: wordsBillingUnits,
},
],
});
const customer = await autumn.customers.get(customerId);
const proProduct = customer.products.find((p) => p.id === pro.id);
const invoices = customer.invoices;
expectProductAttached({
customer,
product: pro,
});
expectProductAttached({
customer,
product: addOn,
});
const expectedPrice = wordsBillingUnits * prepaidWordsItem.price!;
const proratedPrice = calculateProrationAmount({
amount: expectedPrice,
periodStart: new Date().getTime(),
periodEnd: addMonths(new Date(), 2).getTime(),
now: curUnix!,
});
expect(invoices[0].product_ids).toContain(addOn.id);
expect(invoices[0].total).toBeCloseTo(proratedPrice, 1);
const expectedAddonEnd = addMonths(new Date(), 2);
const approximate = 1000 * 60 * 60 * 24; // +- 1 day
const addOnProduct = customer.products.find((p) => p.id === addOn.id);
expect(addOnProduct?.current_period_end).toBeCloseTo(
expectedAddonEnd.getTime(),
-Math.log10(approximate),
);
});
});

View File

@@ -0,0 +1,149 @@
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import { addMonths } from "date-fns";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import {
expectDowngradeCorrect,
expectNextCycleCorrect,
} from "tests/utils/expectUtils/expectScheduleUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
const testCase = "customInterval4";
export const pro = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Words,
includedUsage: 500,
}),
],
intervalCount: 2,
type: "pro",
});
export const premium = constructProduct({
id: "premium",
items: [
constructFeatureItem({
featureId: TestFeature.Words,
includedUsage: 500,
}),
],
intervalCount: 2,
type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrades for custom intervals`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
addPrefixToProducts({
products: [pro, premium],
prefix: testCase,
});
await createProducts({
autumn,
products: [pro, premium],
db,
orgId: org.id,
env,
});
testClockId = testClockId1!;
});
it("should attach premium product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli,
db,
org,
env,
});
});
it("should have correct next cycle at on checkout", async () => {
const checkout = await autumn.checkout({
customer_id: customerId,
product_id: pro.id,
});
const expectedNextCycle = addMonths(new Date(), 2);
expect(checkout.next_cycle?.starts_at).to.be.approximately(
expectedNextCycle.getTime(),
1000 * 60 * 60 * 24,
);
expect(checkout.total).to.equal(0);
});
let preview: any;
it("should downgrade to pro", async () => {
const { preview: preview_ } = await expectDowngradeCorrect({
autumn,
customerId,
curProduct: premium,
newProduct: pro,
stripeCli,
db,
org,
env,
});
preview = preview_;
});
it("should have pro attached on next cycle", async () => {
await expectNextCycleCorrect({
preview: preview!,
autumn,
stripeCli,
customerId,
testClockId,
product: pro,
db,
org,
env,
});
const customer = await autumn.customers.get(customerId);
const invoices = customer.invoices;
expect(invoices.length).to.equal(2);
expect(invoices[0].total).to.equal(getBasePrice({ product: pro }));
});
});

View File

@@ -0,0 +1,133 @@
import { LegacyVersion } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addMonths } from "date-fns";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import {
expectDowngradeCorrect,
expectNextCycleCorrect,
} from "tests/utils/expectUtils/expectScheduleUtils.js";
import { getBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const testCase = "customInterval4";
export const pro = constructProduct({
items: [
constructFeatureItem({
featureId: TestFeature.Words,
includedUsage: 500,
}),
],
intervalCount: 2,
type: "pro",
});
export const premium = constructProduct({
id: "premium",
items: [
constructFeatureItem({
featureId: TestFeature.Words,
includedUsage: 500,
}),
],
intervalCount: 2,
type: "premium",
});
describe(`${chalk.yellowBright(`${testCase}: Testing downgrades for custom intervals`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let stripeCli: Stripe;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro, premium],
prefix: testCase,
customerId,
});
testClockId = testClockId1!;
});
test("should attach premium product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: premium,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
test("should have correct next cycle at on checkout", async () => {
const checkout = await autumn.checkout({
customer_id: customerId,
product_id: pro.id,
});
const expectedNextCycle = addMonths(new Date(), 2);
expect(checkout.next_cycle?.starts_at).toBeCloseTo(
expectedNextCycle.getTime(),
-Math.log10(1000 * 60 * 60 * 24),
);
expect(checkout.total).toBe(0);
});
let preview: any;
test("should downgrade to pro", async () => {
const { preview: preview_ } = await expectDowngradeCorrect({
autumn,
customerId,
curProduct: premium,
newProduct: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
preview = preview_;
});
test("should have pro attached on next cycle", async () => {
await expectNextCycleCorrect({
preview: preview!,
autumn,
stripeCli: ctx.stripeCli,
customerId,
testClockId,
product: pro,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
const customer = await autumn.customers.get(customerId);
const invoices = customer.invoices;
expect(invoices.length).toBe(2);
expect(invoices[0].total).toBe(getBasePrice({ product: pro }));
});
});

View File

@@ -0,0 +1,161 @@
import { type AppEnv, LegacyVersion, type Organization } from "@autumn/shared";
import type { Customer } from "autumn-js";
import { expect } from "chai";
import chalk from "chalk";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
const testCase = "customInterval5";
const includedUsage = 500;
const monthlyWords = constructFeatureItem({
featureId: TestFeature.Words,
includedUsage,
});
const biMonthlyWords = constructFeatureItem({
featureId: TestFeature.Words,
intervalCount: 2,
includedUsage,
});
export const pro = constructProduct({
items: [monthlyWords, biMonthlyWords],
intervalCount: 2,
type: "pro",
});
const getBreakdown = ({
customer,
intervalCount,
}: {
customer: Customer;
intervalCount: number;
}) => {
const wordsFeature = customer.features[TestFeature.Words];
return wordsFeature.breakdown?.find(
(b: any) => b.interval_count === intervalCount,
);
};
describe(`${chalk.yellowBright(`${testCase}: Testing multi interval features with custom intervals`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
addPrefixToProducts({
products: [pro],
prefix: testCase,
});
await createProducts({
autumn,
products: [pro],
db,
orgId: org.id,
env,
});
testClockId = testClockId1!;
});
it("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
});
const customer = await autumn.customers.get(customerId);
const wordsFeature = customer.features[TestFeature.Words];
// @ts-expect-error
expect(wordsFeature.interval_count).to.equal(null);
expect(wordsFeature.breakdown?.length).to.equal(2);
expect(
wordsFeature.breakdown?.some(
(b: any) => b.interval_count === 1 && b.interval === "month",
),
).to.equal(true);
expect(
wordsFeature.breakdown?.some(
(b: any) => b.interval_count === 2 && b.interval === "month",
),
).to.equal(true);
});
const trackVal = 300;
it("should have correct breakdown after usage", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: trackVal,
});
await timeout(3000);
const customer = await autumn.customers.get(customerId);
// Should deduct
const monthlyBreakdown = getBreakdown({ customer, intervalCount: 1 });
const biMonthlyBreakdown = getBreakdown({ customer, intervalCount: 2 });
expect(monthlyBreakdown?.balance).to.equal(includedUsage - trackVal);
expect(biMonthlyBreakdown?.balance).to.equal(includedUsage);
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: trackVal,
});
await timeout(3000);
const customer2 = await autumn.customers.get(customerId);
const monthlyBreakdown2 = getBreakdown({
customer: customer2,
intervalCount: 1,
});
const biMonthlyBreakdown2 = getBreakdown({
customer: customer2,
intervalCount: 2,
});
expect(monthlyBreakdown2?.balance).to.equal(0);
expect(biMonthlyBreakdown2?.balance).to.equal(includedUsage - 100);
});
});

View File

@@ -0,0 +1,145 @@
import { LegacyVersion } from "@autumn/shared";
import type { Customer } from "autumn-js";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const testCase = "customInterval5";
const includedUsage = 500;
const monthlyWords = constructFeatureItem({
featureId: TestFeature.Words,
includedUsage,
});
const biMonthlyWords = constructFeatureItem({
featureId: TestFeature.Words,
intervalCount: 2,
includedUsage,
});
export const pro = constructProduct({
items: [monthlyWords, biMonthlyWords],
intervalCount: 2,
type: "pro",
});
const getBreakdown = ({
customer,
intervalCount,
}: {
customer: Customer;
intervalCount: number;
}) => {
const wordsFeature = customer.features[TestFeature.Words];
return wordsFeature.breakdown?.find(
(b: any) => b.interval_count === intervalCount,
);
};
describe(`${chalk.yellowBright(`${testCase}: Testing multi interval features with custom intervals`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let stripeCli: Stripe;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
customerId,
});
testClockId = testClockId1!;
});
test("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
const customer = await autumn.customers.get(customerId);
const wordsFeature = customer.features[TestFeature.Words];
// @ts-expect-error
expect(wordsFeature.interval_count).toBe(null);
expect(wordsFeature.breakdown?.length).toBe(2);
expect(
wordsFeature.breakdown?.some(
(b: any) => b.interval_count === 1 && b.interval === "month",
),
).toBe(true);
expect(
wordsFeature.breakdown?.some(
(b: any) => b.interval_count === 2 && b.interval === "month",
),
).toBe(true);
});
const trackVal = 300;
test("should have correct breakdown after usage", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: trackVal,
});
await timeout(3000);
const customer = await autumn.customers.get(customerId);
// Should deduct
const monthlyBreakdown = getBreakdown({ customer, intervalCount: 1 });
const biMonthlyBreakdown = getBreakdown({ customer, intervalCount: 2 });
expect(monthlyBreakdown?.balance).toBe(includedUsage - trackVal);
expect(biMonthlyBreakdown?.balance).toBe(includedUsage);
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Words,
value: trackVal,
});
await timeout(3000);
const customer2 = await autumn.customers.get(customerId);
const monthlyBreakdown2 = getBreakdown({
customer: customer2,
intervalCount: 1,
});
const biMonthlyBreakdown2 = getBreakdown({
customer: customer2,
intervalCount: 2,
});
expect(monthlyBreakdown2?.balance).toBe(0);
expect(biMonthlyBreakdown2?.balance).toBe(includedUsage - 100);
});
});

View File

@@ -39,7 +39,7 @@ describe(`${chalk.yellowBright(`advanced/${testCase}: ensure default trials are
const curUnix = Math.floor(new Date().getTime() / 1000);
before(async function () {
beforeAll(async function () {
await setupBefore(this);
await setupDefaultTrialBefore({});
const { autumnJs } = this;

View File

@@ -35,7 +35,7 @@ describe(`${chalk.yellowBright(`advanced/${testCase}: ensure trial transitions i
const curUnix = Math.floor(new Date().getTime() / 1000);
before(async function () {
beforeAll(async function () {
await setupBefore(this);
await setupDefaultTrialBefore({});
const { autumnJs } = this;

View File

@@ -35,7 +35,7 @@ describe(`${chalk.yellowBright(`advanced/${testCase}: ensure trials cancel with
const curUnix = Math.floor(new Date().getTime() / 1000);
before(async function () {
beforeAll(async function () {
await setupBefore(this);
await setupDefaultTrialBefore({});
const { autumnJs } = this;

View File

@@ -0,0 +1,236 @@
import {
type AppEnv,
CusExpand,
CusProductStatus,
ErrCode,
type Organization,
type ReferralCode,
type RewardRedemption,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type { Stripe } from "stripe";
import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
import { CusService } from "@/internal/customers/CusService.js";
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { products, referralPrograms } from "../../../global.js";
export const group = "referrals13";
describe(`${chalk.yellowBright(
"referrals13: Testing referrals - referrer on Pro, gets discount on next cycle - coupon-based",
)}`, () => {
const mainCustomerId = "main-referral-13";
const redeemer = "referral13-r1";
const redeemerPM = "success";
const autumn: AutumnInt = new AutumnInt();
let stripeCli: Stripe;
const testClockIds: string[] = [];
let referralCode: ReferralCode;
let redemption: RewardRedemption;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
db = ctx.db;
org = ctx.org;
env = ctx.env;
try {
await Promise.all([
autumn.customers.delete(mainCustomerId),
autumn.customers.delete(redeemer),
RewardRedemptionService._resetCustomerRedemptions({
db,
internalCustomerId: [mainCustomerId, redeemer],
}),
]);
} catch {}
// Initialize main customer with Pro product already attached
const res = await initCustomerV3({
ctx,
customerId: mainCustomerId,
attachPm: "success",
});
testClockIds.push(res.testClockId);
// Attach Pro product to main customer first
await autumn.attach({
customer_id: mainCustomerId,
product_id: products.pro.id,
});
const redeemerRes = await initCustomerV3({
ctx,
customerId: redeemer,
attachPm: redeemerPM as "success",
withTestClock: true,
});
testClockIds.push(redeemerRes.testClockId);
});
test("should advance clock 10 days before redeeming", async () => {
// Advance 10 days after Pro is attached
await Promise.all(
testClockIds.map((x) =>
advanceTestClock({
testClockId: x,
numberOfDays: 10,
waitForSeconds: 10,
stripeCli,
}),
),
);
});
test("should create code once", async () => {
referralCode = await autumn.referrals.createCode({
customerId: mainCustomerId,
referralId: referralPrograms.paidProductImmediateReferrer.id,
});
expect(referralCode.code).toBeDefined();
});
test("should create redemption for redeemer and fail if redeemed again", async () => {
redemption = await autumn.referrals.redeem({
customerId: redeemer,
code: referralCode.code,
});
// Try redeem for redeemer again
try {
await autumn.referrals.redeem({
customerId: redeemer,
code: referralCode.code,
});
throw new Error("Should not be able to redeem again");
} catch (error) {
expect(error).toBeInstanceOf(AutumnError);
expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode);
}
});
test("should have referrer already on Pro, and redeemer gets free product", async () => {
const redemptionResult = await autumn.redemptions.get(redemption.id);
expect(redemptionResult.redeemer_applied).toBe(true);
const mainProds = (await autumn.customers.get(mainCustomerId)).products;
const redeemerProds = (await autumn.customers.get(redeemer)).products;
// Main customer (referrer) should have the pro product (already attached)
expect(mainProds.length).toBe(1);
expect(mainProds[0].id).toBe(products.pro.id);
// Redeemer should only have the free product (no pro product given in referrer-only program)
expect(redeemerProds.length).toBe(1);
expect(redeemerProds[0].id).toBe(products.free.id);
expectProductV1Attached({
customer: await autumn.customers.get(mainCustomerId),
product: products.pro,
status: CusProductStatus.Active,
});
// Verify redeemer only has free product
expectProductV1Attached({
customer: await autumn.customers.get(redeemer),
product: products.free,
status: CusProductStatus.Active,
});
});
test("should advance test clock and verify referrer gets discount on next Pro cycle", async () => {
// Advance 31 days from current time to trigger next billing cycle
// Coupon was applied on day 10, lasts 30 days, so should still be active on day 31
await Promise.all(
testClockIds.map((x) =>
advanceTestClock({
testClockId: x,
numberOfDays: 31,
waitForSeconds: 25,
stripeCli,
}),
),
);
// Test that main customer's Pro invoice has discount applied
const mainCustomerWithInvoices = await autumn.customers.get(
mainCustomerId,
{
expand: [CusExpand.Invoices, CusExpand.Rewards],
},
);
const proInvoice = mainCustomerWithInvoices.invoices.find((x) =>
x.product_ids.includes(products.pro.id),
);
const expectedTotal = products.pro.prices[0].config.amount;
const actualTotal = proInvoice?.total;
if (proInvoice) {
// Should have a discount applied - invoice total should be less than full Pro price ($10)
expect(actualTotal!).toBeLessThan(expectedTotal);
// For referrer-only reward, the discount should make it significantly cheaper or free
expect(actualTotal!).toBeLessThanOrEqual(expectedTotal / 2);
}
const dbCustomers = await Promise.all(
[mainCustomerId, redeemer].map((x) =>
CusService.getFull({
db,
idOrInternalId: x,
orgId: org.id,
env,
inStatuses: [
CusProductStatus.Active,
CusProductStatus.PastDue,
CusProductStatus.Expired,
],
}),
),
);
const expectedProducts = [
[
// Main referrer - keeps Pro with discount applied
{ name: "Free", status: CusProductStatus.Expired },
{ name: "Pro", status: CusProductStatus.Active },
],
[
// Redeemer - only has free product (no reward in referrer-only program)
{ name: "Free", status: CusProductStatus.Active },
],
];
dbCustomers.forEach((customer, index) => {
const expectedProductsForCustomer = expectedProducts[index];
expectedProductsForCustomer.forEach((expectedProduct) => {
const matchingProduct = customer.customer_products.find(
(cp) =>
cp.product.name === expectedProduct.name &&
cp.status === expectedProduct.status,
);
const unMatchedProduct = customer.customer_products.find(
(cp) => cp.product.name === expectedProduct.name,
);
expect(matchingProduct).toBeDefined();
});
});
});
});

View File

@@ -0,0 +1,245 @@
import {
type AppEnv,
CusExpand,
CusProductStatus,
ErrCode,
type Organization,
type ReferralCode,
type RewardRedemption,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type { Stripe } from "stripe";
import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
import { CusService } from "@/internal/customers/CusService.js";
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { products, referralPrograms } from "../../../global.js";
export const group = "referrals14";
describe(`${chalk.yellowBright(
"referrals14: Testing referrals - referrer on Premium (higher tier), gets pro_amount discount - coupon-based",
)}`, () => {
const mainCustomerId = "main-referral-14";
const redeemer = "referral14-r1";
const redeemerPM = "success";
const autumn: AutumnInt = new AutumnInt();
let stripeCli: Stripe;
const testClockIds: string[] = [];
let referralCode: ReferralCode;
let redemption: RewardRedemption;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
db = ctx.db;
org = ctx.org;
env = ctx.env;
try {
await Promise.all([
autumn.customers.delete(mainCustomerId, { deleteInStripe: true }),
autumn.customers.delete(redeemer, { deleteInStripe: true }),
RewardRedemptionService._resetCustomerRedemptions({
db,
internalCustomerId: [mainCustomerId, redeemer],
}),
]);
} catch {}
// Initialize main customer with Premium product already attached
const res = await initCustomerV3({
ctx,
customerId: mainCustomerId,
attachPm: "success",
});
testClockIds.push(res.testClockId);
// Attach Premium product to main customer first (higher tier than Pro)
await autumn.attach({
customer_id: mainCustomerId,
product_id: products.premium.id,
});
const redeemerRes = await initCustomerV3({
ctx,
customerId: redeemer,
attachPm: redeemerPM as "success",
withTestClock: true,
});
testClockIds.push(redeemerRes.testClockId);
// Advance 10 days after Premium is attached, then redeem the code
await Promise.all(
testClockIds.map((x) =>
advanceTestClock({
testClockId: x,
numberOfDays: 10,
waitForSeconds: 5,
stripeCli,
}),
),
);
});
test("should create code once", async () => {
referralCode = await autumn.referrals.createCode({
customerId: mainCustomerId,
referralId: referralPrograms.paidProductImmediateReferrer.id,
});
expect(referralCode.code).toBeDefined();
// Get referral code again
const referralCode2 = await autumn.referrals.createCode({
customerId: mainCustomerId,
referralId: referralPrograms.paidProductImmediateReferrer.id,
});
expect(referralCode2.code).toBe(referralCode.code);
});
test("should create redemption for redeemer and fail if redeemed again", async () => {
redemption = await autumn.referrals.redeem({
customerId: redeemer,
code: referralCode.code,
});
// Try redeem for redeemer again
try {
await autumn.referrals.redeem({
customerId: redeemer,
code: referralCode.code,
});
throw new Error("Should not be able to redeem again");
} catch (error) {
expect(error).toBeInstanceOf(AutumnError);
expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode);
}
});
test("should have referrer already on Premium, and redeemer gets free product", async () => {
const redemptionResult = await autumn.redemptions.get(redemption.id);
expect(redemptionResult.redeemer_applied).toBe(true);
const mainCus = await autumn.customers.get(mainCustomerId);
const redeemerCus = await autumn.customers.get(redeemer);
const mainProds = mainCus.products;
const redeemerProds = redeemerCus.products;
// Main customer (referrer) should have the premium product (already attached)
expect(mainProds.length).toBe(1);
expect(mainProds[0].id).toBe(products.premium.id);
// Redeemer should only have the free product (no pro product given in referrer-only program)
expect(redeemerProds.length).toBe(1);
expect(redeemerProds[0].id).toBe(products.free.id);
expectProductV1Attached({
customer: mainCus,
product: products.premium,
status: CusProductStatus.Active,
});
// Verify redeemer only has free product
expectProductV1Attached({
customer: redeemerCus,
product: products.free,
status: CusProductStatus.Active,
});
});
test("should advance test clock and verify referrer gets pro_amount discount on Premium cycle", async () => {
// Advance 21 more days (total 31 days from start) to trigger next billing cycle
// Coupon was applied on day 10, lasts 30 days, so should still be active on day 31
await Promise.all(
testClockIds.map((x) =>
advanceTestClock({
testClockId: x,
numberOfDays: 31,
waitForSeconds: 25,
stripeCli,
}),
),
);
// Test that main customer's Premium invoice has pro_amount discount applied
const mainCustomerWithInvoices = await autumn.customers.get(
mainCustomerId,
{
expand: [CusExpand.Invoices],
},
);
const premiumInvoice = mainCustomerWithInvoices.invoices.find((x) =>
x.product_ids.includes(products.premium.id),
);
if (premiumInvoice) {
// Premium costs $50, Pro costs $10 - so referrer should get $10 discount on Premium
// Expected: Premium ($50) - Pro amount ($10) = $40
const premiumPrice = products.premium.prices[0].config.amount; // $50
const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount)
const expectedTotal = premiumPrice - proAmount; // $40
// The invoice total should be exactly Premium price minus pro_amount
expect(premiumInvoice.total).toBe(expectedTotal);
// Verify that the discount was applied (total is less than full Premium price)
expect(premiumInvoice.total).toBeLessThan(premiumPrice);
}
const dbCustomers = await Promise.all(
[mainCustomerId, redeemer].map((x) =>
CusService.getFull({
db,
idOrInternalId: x,
orgId: org.id,
env,
inStatuses: [
CusProductStatus.Active,
CusProductStatus.PastDue,
CusProductStatus.Expired,
],
}),
),
);
const expectedProducts = [
[
// Main referrer - keeps Premium with pro_amount discount applied
{ name: "Free", status: CusProductStatus.Expired },
{ name: "Premium", status: CusProductStatus.Active },
],
[
// Redeemer - only has free product (no reward in referrer-only program)
{ name: "Free", status: CusProductStatus.Active },
],
];
dbCustomers.forEach((customer, index) => {
const expectedProductsForCustomer = expectedProducts[index];
expectedProductsForCustomer.forEach((expectedProduct) => {
const matchingProduct = customer.customer_products.find(
(cp) =>
cp.product.name === expectedProduct.name &&
cp.status === expectedProduct.status,
);
const unMatchedProduct = customer.customer_products.find(
(cp) => cp.product.name === expectedProduct.name,
);
expect(matchingProduct).toBeDefined();
});
});
});
});

View File

@@ -0,0 +1,252 @@
import {
type AppEnv,
CusExpand,
CusProductStatus,
ErrCode,
type Organization,
type ReferralCode,
type RewardRedemption,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type { Stripe } from "stripe";
import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
import { CusService } from "@/internal/customers/CusService.js";
import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { products, referralPrograms } from "../../../global.js";
export const group = "referrals15";
describe(`${chalk.yellowBright(
"referrals15: Testing referrals - referrer starts with no product, gets pro_amount discount - immediate, both - coupon-based",
)}`, () => {
const mainCustomerId = "main-referral-15";
const redeemer = "referral15-r1";
const redeemerPM = "success";
const autumn: AutumnInt = new AutumnInt();
let stripeCli: Stripe;
const testClockIds: string[] = [];
let referralCode: ReferralCode;
let redemption: RewardRedemption;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
db = ctx.db;
org = ctx.org;
env = ctx.env;
try {
await Promise.all([
autumn.customers.delete(mainCustomerId, { deleteInStripe: true }),
autumn.customers.delete(redeemer, { deleteInStripe: true }),
RewardRedemptionService._resetCustomerRedemptions({
db,
internalCustomerId: [mainCustomerId, redeemer],
}),
]);
} catch {}
// Initialize main customer with NO paid product (just free tier)
const res = await initCustomerV3({
ctx,
customerId: mainCustomerId,
attachPm: "success",
});
testClockIds.push(res.testClockId);
const redeemerRes = await initCustomerV3({
ctx,
customerId: redeemer,
attachPm: redeemerPM as "success",
withTestClock: true,
});
testClockIds.push(redeemerRes.testClockId);
});
test("should advance clock 10 days before redeeming", async () => {
// Advance 10 days after setup
await Promise.all(
testClockIds.map((x) =>
advanceTestClock({
testClockId: x,
numberOfDays: 10,
waitForSeconds: 10,
stripeCli,
}),
),
);
});
test("should create code once", async () => {
referralCode = await autumn.referrals.createCode({
customerId: mainCustomerId,
referralId: referralPrograms.paidProductImmediateAll.id,
});
expect(referralCode.code).toBeDefined();
// Get referral code again
const referralCode2 = await autumn.referrals.createCode({
customerId: mainCustomerId,
referralId: referralPrograms.paidProductImmediateAll.id,
});
expect(referralCode2.code).toBe(referralCode.code);
});
test("should create redemption for redeemer and fail if redeemed again", async () => {
redemption = await autumn.referrals.redeem({
customerId: redeemer,
code: referralCode.code,
});
// Try redeem for redeemer again
try {
await autumn.referrals.redeem({
customerId: redeemer,
code: referralCode.code,
});
throw new Error("Should not be able to redeem again");
} catch (error) {
expect(error).toBeInstanceOf(AutumnError);
expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode);
}
});
test("should have both referrer and redeemer get pro product", async () => {
const redemptionResult = await autumn.redemptions.get(redemption.id);
expect(redemptionResult.redeemer_applied).toBe(true);
const mainCus = await autumn.customers.get(mainCustomerId);
const redeemerCus = await autumn.customers.get(redeemer);
const mainProds = mainCus.products;
const redeemerProds = redeemerCus.products;
// Main customer (referrer) should now have the pro product
expect(mainProds.length).toBe(1);
expect(mainProds[0].id).toBe(products.pro.id);
// Redeemer should also have the pro product (both get reward)
expect(redeemerProds.length).toBe(1);
expect(redeemerProds[0].id).toBe(products.pro.id);
expectProductV1Attached({
customer: mainCus,
product: products.pro,
status: CusProductStatus.Active,
});
expectProductV1Attached({
customer: redeemerCus,
product: products.pro,
status: CusProductStatus.Active,
});
});
test("should advance test clock and verify both customers get pro_amount discount on Pro cycle", async () => {
// Advance 31 days from current time to trigger next billing cycle
// Coupon was applied on day 10, lasts 30 days, so should still be active on day 31
await Promise.all(
testClockIds.map((x) =>
advanceTestClock({
testClockId: x,
numberOfDays: 31,
waitForSeconds: 25,
stripeCli,
}),
),
);
// Test that both customers' Pro invoices have pro_amount discount applied
const [mainCustomerWithInvoices, redeemerWithInvoices] = await Promise.all([
autumn.customers.get(mainCustomerId, {
expand: [CusExpand.Invoices],
}),
autumn.customers.get(redeemer, {
expand: [CusExpand.Invoices],
}),
]);
// Check main customer (referrer) invoice
const mainProInvoice = mainCustomerWithInvoices.invoices.find((x) =>
x.product_ids.includes(products.pro.id),
);
if (mainProInvoice) {
// Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0)
const proPrice = products.pro.prices[0].config.amount; // $10
const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount)
const expectedTotal = proPrice - proAmount; // $0
expect(mainProInvoice.total).toBe(expectedTotal);
}
// Check redeemer invoice
const redeemerProInvoice = redeemerWithInvoices.invoices.find((x) =>
x.product_ids.includes(products.pro.id),
);
if (redeemerProInvoice) {
// Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0)
const proPrice = products.pro.prices[0].config.amount; // $10
const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount)
const expectedTotal = proPrice - proAmount; // $0
expect(redeemerProInvoice.total).toBe(expectedTotal);
}
const dbCustomers = await Promise.all(
[mainCustomerId, redeemer].map((x) =>
CusService.getFull({
db,
idOrInternalId: x,
orgId: org.id,
env,
inStatuses: [
CusProductStatus.Active,
CusProductStatus.PastDue,
CusProductStatus.Expired,
],
}),
),
);
const expectedProducts = [
[
// Main referrer - has Pro with pro_amount discount applied
{ name: "Free", status: CusProductStatus.Expired },
{ name: "Pro", status: CusProductStatus.Active },
],
[
// Redeemer - also has Pro with pro_amount discount applied
{ name: "Free", status: CusProductStatus.Expired },
{ name: "Pro", status: CusProductStatus.Active },
],
];
dbCustomers.forEach((customer, index) => {
const expectedProductsForCustomer = expectedProducts[index];
expectedProductsForCustomer.forEach((expectedProduct) => {
const matchingProduct = customer.customer_products.find(
(cp) =>
cp.product.name === expectedProduct.name &&
cp.status === expectedProduct.status,
);
const unMatchedProduct = customer.customer_products.find(
(cp) => cp.product.name === expectedProduct.name,
);
expect(matchingProduct).toBeDefined();
});
});
});
});

View File

@@ -0,0 +1,326 @@
// NOTE: This test is commented out in the original file (referrals16.ts)
// Keeping it commented out in the migrated version as well
// import {
// type AppEnv,
// CusExpand,
// CusProductStatus,
// ErrCode,
// type Organization,
// type ReferralCode,
// type RewardRedemption,
// } from "@autumn/shared";
// import { beforeAll, describe, expect, test } from "bun:test";
// import chalk from "chalk";
// import type { Stripe } from "stripe";
// import { expectProductV1Attached } from "tests/utils/expectUtils/expectProductAttached.js";
// import {
// advanceTestClock,
// completeCheckoutForm,
// } from "tests/utils/stripeUtils.js";
// import ctx from "tests/utils/testInitUtils/createTestContext.js";
// import type { DrizzleCli } from "@/db/initDrizzle.js";
// import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
// import { CusService } from "@/internal/customers/CusService.js";
// import { RewardRedemptionService } from "@/internal/rewards/RewardRedemptionService.js";
// import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
// import { products, referralPrograms, rewards } from "../../../global.js";
// export const group = "referrals16";
// describe(`${chalk.yellowBright(
// "referrals16: Testing referrals - referrer starts with no product, gets pro_amount discount - checkout, both - coupon-based"
// )}`, () => {
// const mainCustomerId = "main-referral-16";
// const redeemer = "referral16-r1";
// const redeemerPM = "success";
// const autumn: AutumnInt = new AutumnInt();
// let stripeCli: Stripe;
// const testClockIds: string[] = [];
// let referralCode: ReferralCode;
// let redemption: RewardRedemption;
// let db: DrizzleCli;
// let org: Organization;
// let env: AppEnv;
// beforeAll(async () => {
// stripeCli = ctx.stripeCli;
// db = ctx.db;
// org = ctx.org;
// env = ctx.env;
// try {
// await Promise.all([
// autumn.customers.delete(mainCustomerId, { deleteInStripe: true }),
// autumn.customers.delete(redeemer, { deleteInStripe: true }),
// RewardRedemptionService._resetCustomerRedemptions({
// db,
// internalCustomerId: [mainCustomerId, redeemer],
// }),
// ]);
// } catch {}
// // Initialize main customer with NO paid product (just free tier)
// const res = await initCustomerV3({
// ctx,
// customerId: mainCustomerId,
// attachPm: "success",
// });
// testClockIds.push(res.testClockId);
// const redeemerRes = await initCustomerV3({
// ctx,
// customerId: redeemer,
// attachPm: redeemerPM as "success",
// withTestClock: true,
// });
// testClockIds.push(redeemerRes.testClockId);
// });
// test("should advance clock 10 days before redeeming", async () => {
// // Advance 10 days after setup
// await Promise.all(
// testClockIds.map((x) =>
// advanceTestClock({
// testClockId: x,
// numberOfDays: 10,
// waitForSeconds: 10,
// stripeCli,
// })
// )
// );
// });
// test("should create code once", async () => {
// referralCode = await autumn.referrals.createCode({
// customerId: mainCustomerId,
// referralId: referralPrograms.paidProductCheckoutAll.id,
// });
// expect(referralCode.code).toBeDefined();
// // Get referral code again
// const referralCode2 = await autumn.referrals.createCode({
// customerId: mainCustomerId,
// referralId: referralPrograms.paidProductCheckoutAll.id,
// });
// expect(referralCode2.code).toBe(referralCode.code);
// });
// test("should create redemption for redeemer and fail if redeemed again", async () => {
// redemption = await autumn.referrals.redeem({
// customerId: redeemer,
// code: referralCode.code,
// });
// // Try redeem for redeemer again
// try {
// await autumn.referrals.redeem({
// customerId: redeemer,
// code: referralCode.code,
// });
// throw new Error("Should not be able to redeem again");
// } catch (error) {
// expect(error).toBeInstanceOf(AutumnError);
// expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode);
// }
// });
// test("should have referrer and redeemer still on free tier (reward not triggered yet)", async () => {
// const redemptionResult = await autumn.redemptions.get(redemption.id);
// expect(redemptionResult.triggered).toBe(false); // Checkout trigger not fired yet
// const mainCus = await autumn.customers.get(mainCustomerId);
// const redeemerCus = await autumn.customers.get(redeemer);
// const mainProds = mainCus.products;
// const redeemerProds = redeemerCus.products;
// // Both customers should still only have free product
// expect(mainProds.length).toBe(1);
// expect(mainProds[0].id).toBe(products.free.id);
// expect(redeemerProds.length).toBe(1);
// expect(redeemerProds[0].id).toBe(products.free.id);
// expectProductV1Attached({
// customer: mainCus,
// product: products.free,
// status: CusProductStatus.Active,
// });
// expectProductV1Attached({
// customer: redeemerCus,
// product: products.free,
// status: CusProductStatus.Active,
// });
// });
// test("should trigger reward when redeemer checks out with Premium", async () => {
// // Redeemer purchases Premium product (triggers checkout reward)
// const checkoutRes = await autumn.attach({
// customer_id: redeemer,
// product_id: products.premium.id,
// force_checkout: true,
// });
// await completeCheckoutForm(checkoutRes.checkout_url);
// // Wait a bit for webhook processing
// await new Promise((resolve) => setTimeout(resolve, 10000));
// // Now both customers should have the reward applied
// const redemptionResult = await autumn.redemptions.get(redemption.id);
// expect(redemptionResult.applied).toBe(true);
// const mainCus = await autumn.customers.get(mainCustomerId);
// const redeemerCus = await autumn.customers.get(redeemer);
// const mainProds = mainCus.products;
// const redeemerProds = redeemerCus.products;
// // Main customer (referrer) should now have the pro product
// expect(mainProds.length).toBe(1);
// expect(mainProds[0].id).toBe(products.pro.id);
// // Redeemer should have both Premium (purchased) and the Pro price discount
// expect(redeemerProds.length).toBe(1);
// const redeemerStripeDiscounts = await stripeCli.subscriptions.retrieve(
// redeemerProds.find((x) => x.id === products.premium.id)
// ?.subscription_ids?.[0]!,
// {
// expand: ["discounts"],
// }
// );
// const parsedDiscountID = redeemerStripeDiscounts.discounts.find((x) => {
// if (typeof x === "string") {
// return x;
// } else if (typeof x === "object") {
// return x.coupon.id;
// } else return null;
// })!;
// expect(redeemerStripeDiscounts.discounts.length).toBe(1);
// expect(
// typeof parsedDiscountID === "object"
// ? parsedDiscountID.coupon.id
// : parsedDiscountID
// ).toBe(rewards.paidProductWithConfig.id);
// expect(
// redeemerProds.find((x) => x.id === products.premium.id)
// ).toBeDefined();
// expectProductV1Attached({
// customer: mainCus,
// product: products.pro,
// status: CusProductStatus.Active,
// });
// expectProductV1Attached({
// customer: redeemerCus,
// product: products.premium,
// status: CusProductStatus.Active,
// });
// });
// test("should advance test clock and verify both customers get pro_amount discount on their cycles", async () => {
// // Advance 31 days from current time to trigger next billing cycle
// await Promise.all(
// testClockIds.map((x) =>
// advanceTestClock({
// testClockId: x,
// numberOfDays: 31,
// waitForSeconds: 25,
// stripeCli,
// })
// )
// );
// // Test that both customers' invoices have pro_amount discount applied
// const [mainCustomerWithInvoices, redeemerWithInvoices] = await Promise.all([
// autumn.customers.get(mainCustomerId, {
// expand: [CusExpand.Invoices],
// }),
// autumn.customers.get(redeemer, {
// expand: [CusExpand.Invoices],
// }),
// ]);
// // Check main customer (referrer) Pro invoice
// const mainProInvoice = mainCustomerWithInvoices.invoices.find((x) =>
// x.product_ids.includes(products.pro.id)
// );
// if (mainProInvoice) {
// // Pro costs $10, so with pro_amount discount it should be $0 (Pro - Pro amount = $0)
// const proPrice = products.pro.prices[0].config.amount; // $10
// const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount)
// const expectedTotal = proPrice - proAmount; // $0
// expect(mainProInvoice.total).toBe(expectedTotal);
// }
// // Check redeemer Premium invoice (should have $10 off)
// const redeemerPremiumInvoice = redeemerWithInvoices.invoices.find((x) =>
// x.product_ids.includes(products.premium.id)
// );
// if (redeemerPremiumInvoice) {
// // Premium costs $50, so with pro_amount discount it should be $40 (Premium - Pro amount = $40)
// const premiumPrice = products.premium.prices[0].config.amount; // $50
// const proAmount = products.pro.prices[0].config.amount; // $10 (pro_amount discount)
// const expectedTotal = premiumPrice - proAmount; // $40
// expect(redeemerPremiumInvoice.total).toBe(expectedTotal);
// }
// const dbCustomers = await Promise.all(
// [mainCustomerId, redeemer].map((x) =>
// CusService.getFull({
// db,
// idOrInternalId: x,
// orgId: org.id,
// env,
// inStatuses: [
// CusProductStatus.Active,
// CusProductStatus.PastDue,
// CusProductStatus.Expired,
// ],
// })
// )
// );
// const expectedProducts = [
// [
// // Main referrer - has Pro with pro_amount discount applied
// { name: "Free", status: CusProductStatus.Expired },
// { name: "Pro", status: CusProductStatus.Active },
// ],
// [
// // Redeemer - has both Premium (purchased) and Pro (reward) with discounts
// { name: "Free", status: CusProductStatus.Expired },
// { name: "Pro", status: CusProductStatus.Active },
// { name: "Premium", status: CusProductStatus.Active },
// ],
// ];
// dbCustomers.forEach((customer, index) => {
// const expectedProductsForCustomer = expectedProducts[index];
// expectedProductsForCustomer.forEach((expectedProduct) => {
// const matchingProduct = customer.customer_products.find(
// (cp) =>
// cp.product.name === expectedProduct.name &&
// cp.status === expectedProduct.status
// );
// const unMatchedProduct = customer.customer_products.find(
// (cp) => cp.product.name === expectedProduct.name
// );
// expect(matchingProduct).toBeDefined();
// });
// });
// });
// });

View File

@@ -0,0 +1,220 @@
import {
type AppEnv,
ErrCode,
type Organization,
type ReferralCode,
type RewardRedemption,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addDays } from "date-fns";
import type { Stripe } from "stripe";
import { TestFeature } from "tests/setup/v2Features.js";
import { timeout } from "tests/utils/genUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import { addPrefixToProducts } from "tests/utils/testProductUtils/testProductUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { products, referralPrograms } from "../../global.js";
const pro = constructProduct({
id: "pro",
items: [constructFeatureItem({ featureId: TestFeature.Words })],
type: "pro",
trial: true,
});
describe(`${chalk.yellowBright(
"referrals1: Testing referrals (on checkout)",
)}`, () => {
const mainCustomerId = "main-referral-1";
const alternateCustomerId = "alternate-referral-1";
const redeemers = ["referral1-r1", "referral1-r2", "referral1-r3"];
const autumn: AutumnInt = new AutumnInt();
let stripeCli: Stripe;
let testClockId: string;
let referralCode: ReferralCode;
const redemptions: RewardRedemption[] = [];
let mainCustomer: any;
let db: DrizzleCli;
let org: Organization;
let env: AppEnv;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
db = ctx.db;
org = ctx.org;
env = ctx.env;
addPrefixToProducts({
products: [pro],
prefix: mainCustomerId,
});
await createProducts({
autumn: new AutumnInt({ secretKey: ctx.orgSecretKey }),
products: [pro],
db,
orgId: org.id,
env,
customerId: mainCustomerId,
});
const res = await initCustomerV3({
ctx,
customerId: mainCustomerId,
customerData: { fingerprint: "main-referral-1" },
attachPm: "success",
});
mainCustomer = res.customer;
testClockId = res.testClockId;
await autumn.attach({
customer_id: mainCustomerId,
product_id: pro.id,
});
const batchCreate = [];
for (const redeemer of redeemers) {
batchCreate.push(
initCustomerV3({
ctx,
customerId: redeemer,
attachPm: "success",
}),
);
}
batchCreate.push(
initCustomerV3({
ctx,
customerId: alternateCustomerId,
customerData: { fingerprint: "main-referral-1" },
attachPm: "success",
}),
);
await Promise.all(batchCreate);
});
test("should create code once", async () => {
referralCode = await autumn.referrals.createCode({
customerId: mainCustomerId,
referralId: referralPrograms.onCheckout.id,
});
expect(referralCode.code).toBeDefined();
// Get referral code again
const referralCode2 = await autumn.referrals.createCode({
customerId: mainCustomerId,
referralId: referralPrograms.onCheckout.id,
});
expect(referralCode2.code).toBe(referralCode.code);
});
test("should fail if same customer tries to redeem code again", async () => {
try {
await autumn.referrals.redeem({
customerId: mainCustomerId,
code: referralCode.code,
});
throw new Error("Own customer should not be able to redeem code");
} catch (error) {
expect(error).toBeInstanceOf(AutumnError);
expect((error as AutumnError).code).toBe(ErrCode.CustomerCannotRedeemOwnCode);
}
try {
await autumn.referrals.redeem({
customerId: alternateCustomerId,
code: referralCode.code,
});
throw new Error(
"Own customer (same fingerprint) should not be able to redeem code",
);
} catch (error) {
expect(error).toBeInstanceOf(AutumnError);
expect((error as AutumnError).code).toBe(ErrCode.CustomerCannotRedeemOwnCode);
}
});
test("should create redemption for each redeemer and fail if redeemed again", async () => {
for (const redeemer of redeemers) {
const redemption: RewardRedemption = await autumn.referrals.redeem({
customerId: redeemer,
code: referralCode.code,
});
redemptions.push(redemption);
}
// Try redeem for redeemer1 again
try {
const redemption1 = await autumn.referrals.redeem({
customerId: redeemers[0],
code: referralCode.code,
});
throw new Error("Should not be able to redeem again");
} catch (error) {
expect(error).toBeInstanceOf(AutumnError);
expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode);
}
});
test("should be triggered (and applied) when redeemers check out", async () => {
for (let i = 0; i < redeemers.length; i++) {
const redeemer = redeemers[i];
await autumn.attach({
customer_id: redeemer,
product_id: products.pro.id,
});
await timeout(3000);
// Get redemption object
const redemption = await autumn.redemptions.get(redemptions[i].id);
// Check if redemption is triggered
const count = i + 1;
if (count > referralPrograms.onCheckout.max_redemptions) {
expect(redemption.triggered).toBe(false);
expect(redemption.applied).toBe(false);
} else {
expect(redemption.triggered).toBe(true);
expect(redemption.applied).toBe(i === 0);
}
// Check stripe customer
const stripeCus = (await stripeCli.customers.retrieve(
mainCustomer.processor?.id,
)) as Stripe.Customer;
expect(stripeCus.discount).not.toBe(null);
}
});
let curTime = new Date();
test("customer should have discount for first purchase", async () => {
curTime = addDays(addDays(curTime, 7), 4);
await advanceTestClock({
testClockId,
advanceTo: curTime.getTime(),
stripeCli,
});
// 1. Get invoice
const { invoices } = await autumn.customers.get(mainCustomerId);
expect(invoices.length).toBe(2);
expect(invoices[0].total).toBe(0);
});
});

View File

@@ -0,0 +1,136 @@
import {
type AppEnv,
type Customer,
ErrCode,
type Organization,
type ReferralCode,
type RewardRedemption,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addDays } from "date-fns";
import type { Stripe } from "stripe";
import { timeout } from "tests/utils/genUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
import { createStripeCli } from "@/external/connect/createStripeCli.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { products, referralPrograms } from "../../global.js";
describe(`${chalk.yellowBright(
"referrals2: Testing referrals (immediate redemption)",
)}`, () => {
const mainCustomerId = "main-referral-2";
const redeemers = ["referral2-r1", "referral2-r2", "referral2-r3"];
const autumn: AutumnInt = new AutumnInt();
let stripeCli: Stripe;
let testClockId: string;
let referralCode: ReferralCode;
const redemptions: RewardRedemption[] = [];
let mainCustomer: Customer;
let org: Organization;
let env: AppEnv;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
org = ctx.org;
env = ctx.env;
const { testClockId: testClockId1, customer } = await initCustomerV3({
ctx,
customerId: mainCustomerId,
});
testClockId = testClockId1;
mainCustomer = customer;
const batchCreate = [];
for (const redeemer of redeemers) {
batchCreate.push(
initCustomerV3({
ctx,
customerId: redeemer,
attachPm: "success",
}),
);
}
await Promise.all(batchCreate);
});
test("should create code once", async () => {
referralCode = await autumn.referrals.createCode({
customerId: mainCustomerId,
referralId: referralPrograms.immediate.id,
});
expect(referralCode.code).toBeDefined();
});
test("should create redemption for each redeemer and fail if redeemed again", async () => {
for (let i = 0; i < redeemers.length; i++) {
const redeemer = redeemers[i];
const count = i + 1;
try {
const redemption: RewardRedemption = await autumn.referrals.redeem({
customerId: redeemer,
code: referralCode.code,
});
redemptions.push(redemption);
if (count > referralPrograms.immediate.max_redemptions) {
expect(redemption.triggered).toBe(false);
expect(redemption.applied).toBe(false);
} else {
throw new Error("Should not be able to redeem again");
}
} catch (error) {
if (count > referralPrograms.immediate.max_redemptions) {
expect(error).toBeInstanceOf(AutumnError);
expect((error as AutumnError).code).toBe(ErrCode.ReferralCodeMaxRedemptionsReached);
}
}
}
// Check stripe customer
const legacyStripe = createStripeCli({
org: org,
env: env,
legacyVersion: true,
});
const stripeCus = (await legacyStripe.customers.retrieve(
mainCustomer.processor?.id,
{
expand: ["discount"],
},
)) as Stripe.Customer;
expect(stripeCus.discount).not.toBe(null);
});
let curTime = new Date();
test("customer should have discount for first purchase", async () => {
await autumn.attach({
customer_id: mainCustomerId,
product_id: products.proWithTrial.id,
});
await timeout(3000);
curTime = addDays(addDays(curTime, 7), 4);
await advanceTestClock({
testClockId,
advanceTo: curTime.getTime(),
stripeCli,
waitForSeconds: 30,
});
// 1. Get invoice
const { invoices } = await autumn.customers.get(mainCustomerId);
expect(invoices!.length).toBe(2);
expect(invoices![0].total).toBe(0);
});
});

View File

@@ -0,0 +1,130 @@
import {
type Customer,
ErrCode,
type ReferralCode,
type RewardRedemption,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type { Stripe } from "stripe";
import { compareProductEntitlements } from "tests/utils/compare.js";
import { timeout } from "tests/utils/genUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import AutumnError, { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { features, products, referralPrograms } from "../../global.js";
describe(`${chalk.yellowBright(
"referrals3: Testing free product referrals",
)}`, () => {
const mainCustomerId = "main-referral-3";
const redeemers = ["referral3-r1", "referral3-r2", "referral3-r3"];
let autumn: AutumnInt = new AutumnInt();
let stripeCli: Stripe;
let testClockId: string;
let referralCode: ReferralCode;
const redemptions: RewardRedemption[] = [];
let mainCustomer: Customer;
beforeAll(async () => {
autumn = new AutumnInt({ secretKey: ctx.orgSecretKey });
stripeCli = ctx.stripeCli;
const { testClockId: testClockId1, customer } = await initCustomerV3({
ctx,
customerId: mainCustomerId,
customerData: { fingerprint: "main-referral-3" },
});
testClockId = testClockId1;
mainCustomer = customer;
await autumn.attach({
customer_id: mainCustomerId,
product_id: products.proWithTrial.id,
});
const batchCreate = [];
for (const redeemer of redeemers) {
batchCreate.push(
initCustomerV3({
ctx,
customerId: redeemer,
attachPm: "success",
}),
);
}
await Promise.all(batchCreate);
});
test("should create code once", async () => {
referralCode = await autumn.referrals.createCode({
customerId: mainCustomerId,
referralId: referralPrograms.freeProduct.id,
});
expect(referralCode.code).toBeDefined();
});
test("should create redemption for each redeemer and fail if redeemed again", async () => {
for (const redeemer of redeemers) {
const redemption: RewardRedemption = await autumn.referrals.redeem({
customerId: redeemer,
code: referralCode.code,
});
redemptions.push(redemption);
}
// Try redeem for redeemer1 again
try {
const redemption1 = await autumn.referrals.redeem({
customerId: redeemers[0],
code: referralCode.code,
});
throw new Error("Should not be able to redeem again");
} catch (error) {
expect(error).toBeInstanceOf(AutumnError);
expect((error as AutumnError).code).toBe(ErrCode.CustomerAlreadyRedeemedReferralCode);
}
});
test("should be triggered (and applied) when redeemers check out", async () => {
for (let i = 0; i < redeemers.length; i++) {
const redeemer = redeemers[i];
await autumn.attach({
customer_id: redeemer,
product_id: products.pro.id,
});
await timeout(3000);
// Get redemption object
const redemption = await autumn.redemptions.get(redemptions[i].id);
// Check if redemption is triggered
const count = i + 1;
if (count > referralPrograms.freeProduct.max_redemptions) {
expect(redemption.triggered).toBe(false);
expect(redemption.applied).toBe(false);
} else {
// 1. Check that main customer has free add on
compareProductEntitlements({
customerId: mainCustomerId,
product: products.freeAddOn,
features,
quantity: count,
});
compareProductEntitlements({
customerId: redeemer,
product: products.freeAddOn,
features,
});
}
}
});
});

View File

@@ -0,0 +1,117 @@
import type { Customer, ReferralCode, RewardRedemption } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addDays, addHours } from "date-fns";
import type { Stripe } from "stripe";
import { compareProductEntitlements } from "tests/utils/compare.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { timeout } from "tests/utils/genUtils.js";
import { advanceTestClock } from "tests/utils/stripeUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { features, products, referralPrograms } from "../../global.js";
describe(`${chalk.yellowBright(
"referrals4: Testing free product referrals with trial",
)}`, () => {
const mainCustomerId = "main-referral-4";
const redeemerId = "referral4-r1";
let autumn: AutumnInt = new AutumnInt();
let stripeCli: Stripe;
let referralCode: ReferralCode;
const redemptions: RewardRedemption[] = [];
let mainCustomer: Customer;
let redeemer: Customer;
let testClockId: string;
beforeAll(async () => {
autumn = new AutumnInt({ secretKey: ctx.orgSecretKey });
stripeCli = ctx.stripeCli;
await initCustomerV3({
ctx,
customerId: mainCustomerId,
attachPm: "success",
});
await autumn.attach({
customer_id: mainCustomerId,
product_id: products.proWithTrial.id,
});
const { testClockId: testClockId1, customer } = await initCustomerV3({
ctx,
customerId: redeemerId,
});
testClockId = testClockId1;
redeemer = customer;
});
test("should create referral code", async () => {
referralCode = await autumn.referrals.createCode({
customerId: mainCustomerId,
referralId: referralPrograms.freeProduct.id,
});
expect(referralCode.code).toBeDefined();
});
test("should create redemption for each redeemer and fail if redeemed again", async () => {
const redemption: RewardRedemption = await autumn.referrals.redeem({
customerId: redeemerId,
code: referralCode.code,
});
redemptions.push(redemption);
});
test("should not be triggered because of trial", async () => {
await autumn.attach({
customer_id: redeemerId,
product_id: products.proWithTrial.id,
});
await timeout(3000);
// Get redemption object
const redemption = await autumn.redemptions.get(redemptions[0].id);
expect(redemption.triggered).toBe(false);
});
test("should be triggered after trial ends", async () => {
const advanceTo = addHours(
addDays(new Date(), 7),
hoursToFinalizeInvoice,
).getTime();
await advanceTestClock({
stripeCli,
testClockId,
advanceTo,
waitForSeconds: 30,
});
const redemption = await autumn.redemptions.get(redemptions[0].id);
expect(redemption.triggered).toBe(true);
compareProductEntitlements({
customerId: mainCustomerId,
product: products.freeAddOn,
features,
quantity: 1,
});
compareProductEntitlements({
customerId: redeemerId,
product: products.freeAddOn,
features,
quantity: 1,
});
});
});

View File

@@ -0,0 +1,198 @@
import {
type AppEnv,
type Customer,
LegacyVersion,
type LimitedItem,
type Organization,
ProductItemInterval,
RolloverDuration,
} from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { resetAndGetCusEnt } from "./rolloverTestUtils.js";
const rolloverConfig = {
max: 500,
length: 1,
duration: RolloverDuration.Month,
};
const messagesItem = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 400,
interval: ProductItemInterval.Month,
rolloverConfig,
}) as LimitedItem;
export const free = constructProduct({
items: [messagesItem],
type: "free",
isDefault: false,
});
const testCase = "rollover1";
// , per entity and regular
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let customer: Customer;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [free],
prefix: testCase,
});
await createProducts({
autumn,
products: [free],
customerId,
db,
orgId: org.id,
env,
});
const res = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = res.testClockId!;
customer = res.customer;
});
it("should attach free product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: free.id,
});
});
const messageUsage = 250;
let curBalance = messagesItem.included_usage;
it("should create track messages, reset, and have correct rollover", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: messageUsage,
});
await timeout(3000);
await resetAndGetCusEnt({
db,
customer,
productGroup: free.group,
featureId: TestFeature.Messages,
});
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
const expectedRollover = Math.min(
messagesItem.included_usage - messageUsage,
rolloverConfig.max,
);
const expectedBalance = messagesItem.included_usage + expectedRollover;
expect(msgesFeature).to.exist;
expect(msgesFeature?.balance).to.equal(expectedBalance);
// @ts-expect-error
expect(msgesFeature?.rollovers[0].balance).to.equal(expectedRollover);
curBalance = expectedBalance;
});
// let usage2 = 50;
it("should reset again and have correct rollover", async () => {
await resetAndGetCusEnt({
db,
customer,
productGroup: free.group,
featureId: TestFeature.Messages,
});
const expectedRollover = Math.min(curBalance, rolloverConfig.max);
const expectedBalance = messagesItem.included_usage + expectedRollover;
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
expect(msgesFeature).to.exist;
expect(msgesFeature?.balance).to.equal(expectedBalance);
// @ts-expect-error (oldest rollover should be 100 (150 - 50))
expect(msgesFeature?.rollovers[0].balance).to.equal(100);
// @ts-expect-error (newest rollover should be 400 (msges.included_usage))
expect(msgesFeature?.rollovers[1].balance).to.equal(400);
});
it("should track messages and deduct from rollovers first", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: 150,
});
await timeout(3000);
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
// @ts-expect-error
const rollover1 = msgesFeature?.rollovers[0];
// @ts-expect-error
const rollover2 = msgesFeature?.rollovers[1];
expect(rollover1.balance).to.equal(0);
expect(rollover2.balance).to.equal(350);
});
it("should track and deduct from rollover + original balance", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: 400,
});
await timeout(3000);
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
// @ts-expect-error
const rollovers = msgesFeature.rollovers;
expect(rollovers![0].balance).to.equal(0);
expect(rollovers![1].balance).to.equal(0);
expect(msgesFeature.balance).to.equal(messagesItem.included_usage - 50);
});
});

View File

@@ -0,0 +1,179 @@
import {
type Customer,
LegacyVersion,
type LimitedItem,
ProductItemInterval,
RolloverDuration,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { resetAndGetCusEnt } from "./rolloverTestUtils.js";
const rolloverConfig = {
max: 500,
length: 1,
duration: RolloverDuration.Month,
};
const messagesItem = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 400,
interval: ProductItemInterval.Month,
rolloverConfig,
}) as LimitedItem;
export const free = constructProduct({
items: [messagesItem],
type: "free",
isDefault: false,
});
const testCase = "rollover1";
// , per entity and regular
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let customer: Customer;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
beforeAll(async () => {
stripeCli = ctx.stripeCli;
await initProductsV0({
ctx,
products: [free],
prefix: testCase,
customerId,
});
const res = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = res.testClockId!;
customer = res.customer;
});
test("should attach free product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: free.id,
});
});
const messageUsage = 250;
let curBalance = messagesItem.included_usage;
test("should create track messages, reset, and have correct rollover", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: messageUsage,
});
await timeout(3000);
await resetAndGetCusEnt({
db: ctx.db,
customer,
productGroup: free.group,
featureId: TestFeature.Messages,
});
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
const expectedRollover = Math.min(
messagesItem.included_usage - messageUsage,
rolloverConfig.max,
);
const expectedBalance = messagesItem.included_usage + expectedRollover;
expect(msgesFeature).toBeDefined();
expect(msgesFeature?.balance).toBe(expectedBalance);
// @ts-expect-error
expect(msgesFeature?.rollovers[0].balance).toBe(expectedRollover);
curBalance = expectedBalance;
});
// let usage2 = 50;
test("should reset again and have correct rollover", async () => {
await resetAndGetCusEnt({
db: ctx.db,
customer,
productGroup: free.group,
featureId: TestFeature.Messages,
});
const expectedRollover = Math.min(curBalance, rolloverConfig.max);
const expectedBalance = messagesItem.included_usage + expectedRollover;
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
expect(msgesFeature).toBeDefined();
expect(msgesFeature?.balance).toBe(expectedBalance);
// @ts-expect-error (oldest rollover should be 100 (150 - 50))
expect(msgesFeature?.rollovers[0].balance).toBe(100);
// @ts-expect-error (newest rollover should be 400 (msges.included_usage))
expect(msgesFeature?.rollovers[1].balance).toBe(400);
});
test("should track messages and deduct from rollovers first", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: 150,
});
await timeout(3000);
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
// @ts-expect-error
const rollover1 = msgesFeature?.rollovers[0];
// @ts-expect-error
const rollover2 = msgesFeature?.rollovers[1];
expect(rollover1.balance).toBe(0);
expect(rollover2.balance).toBe(350);
});
test("should track and deduct from rollover + original balance", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: 400,
});
await timeout(3000);
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
// @ts-expect-error
const rollovers = msgesFeature.rollovers;
expect(rollovers![0].balance).toBe(0);
expect(rollovers![1].balance).toBe(0);
expect(msgesFeature.balance).toBe(messagesItem.included_usage - 50);
});
});

View File

@@ -0,0 +1,225 @@
import {
type AppEnv,
type Customer,
LegacyVersion,
type LimitedItem,
type Organization,
ProductItemInterval,
RolloverDuration,
} from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { resetAndGetCusEnt } from "./rolloverTestUtils.js";
const rolloverConfig = {
max: 500,
length: 1,
duration: RolloverDuration.Month,
};
const msgesItem = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 400,
interval: ProductItemInterval.Month,
rolloverConfig,
entityFeatureId: TestFeature.Users,
}) as LimitedItem;
export const free = constructProduct({
items: [msgesItem],
type: "free",
isDefault: false,
});
const testCase = "rollover2";
// , per entity and regular
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item (per entity)`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let customer: Customer;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [free],
prefix: testCase,
});
await createProducts({
autumn,
products: [free],
customerId,
db,
orgId: org.id,
env,
});
const res = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = res.testClockId!;
customer = res.customer;
});
const entities: any[] = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
];
it("should attach pro product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: free.id,
});
await autumn.entities.create(customerId, entities);
});
const entity1Id = entities[0].id;
const entity2Id = entities[1].id;
const newEntity1Balance = 300;
const newEntity2Balance = 200;
const includedUsage = msgesItem.included_usage;
const usages = [
{
entityId: entity1Id,
usage: includedUsage - newEntity1Balance,
rollover: newEntity1Balance,
},
{
entityId: entity2Id,
usage: includedUsage - newEntity2Balance,
rollover: newEntity2Balance,
},
];
it("should create track messages, reset, and have correct rollover", async () => {
for (const usage of usages) {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: usage.usage,
entity_id: usage.entityId,
});
}
await timeout(3000);
// Run reset cusEnt on ...
await resetAndGetCusEnt({
db,
customer,
productGroup: free.group,
featureId: TestFeature.Messages,
});
for (const usage of usages) {
const entity = await autumn.entities.get(customerId, usage.entityId);
const msgesFeature = entity.features[TestFeature.Messages];
const expectedRollover = Math.min(usage.rollover, rolloverConfig.max);
expect(msgesFeature.rollovers.length).to.equal(1);
expect(msgesFeature.balance).to.equal(includedUsage + expectedRollover);
expect(msgesFeature.rollovers[0].balance).to.equal(expectedRollover);
}
});
it("should reset again and have correct rollovers", async () => {
await resetAndGetCusEnt({
db,
customer,
productGroup: free.group,
featureId: TestFeature.Messages,
});
const entity1 = await autumn.entities.get(customerId, entity1Id);
const entity1Msges = entity1.features[TestFeature.Messages];
// 400, 300 -> 400, 100 (max is 500)
const rollovers = entity1Msges.rollovers;
expect(rollovers[0].balance).to.equal(100);
expect(rollovers[1].balance).to.equal(400);
const entity2 = await autumn.entities.get(customerId, entity2Id);
const entity2Msges = entity2.features[TestFeature.Messages];
// 400, 200 -> 400, 0 (max is 500)
const rollovers2 = entity2Msges.rollovers;
expect(rollovers2[0].balance).to.equal(100);
expect(rollovers2[1].balance).to.equal(400);
});
it("should track and deduct from oldest rollovers first", async () => {
for (const entity of entities) {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: 150,
entity_id: entity.id,
});
await timeout(2000);
const entRes = await autumn.entities.get(customerId, entity.id);
const msgesFeature = entRes.features[TestFeature.Messages];
const rollovers = msgesFeature.rollovers;
expect(rollovers[0].balance).to.equal(0);
expect(rollovers[1].balance).to.equal(350);
expect(msgesFeature.balance).to.equal(includedUsage + 350);
}
});
it("should track past rollovers and deduct from original balance", async () => {
for (const entity of entities) {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: 400,
entity_id: entity.id,
});
await timeout(2000);
const entRes = await autumn.entities.get(customerId, entity.id);
const msgesFeature = entRes.features[TestFeature.Messages];
const rollovers = msgesFeature.rollovers;
expect(rollovers[0].balance).to.equal(0);
expect(rollovers[1].balance).to.equal(0);
expect(msgesFeature.balance).to.equal(includedUsage - 50);
}
});
});

View File

@@ -0,0 +1,206 @@
import {
type Customer,
LegacyVersion,
type LimitedItem,
ProductItemInterval,
RolloverDuration,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { resetAndGetCusEnt } from "./rolloverTestUtils.js";
const rolloverConfig = {
max: 500,
length: 1,
duration: RolloverDuration.Month,
};
const msgesItem = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 400,
interval: ProductItemInterval.Month,
rolloverConfig,
entityFeatureId: TestFeature.Users,
}) as LimitedItem;
export const free = constructProduct({
items: [msgesItem],
type: "free",
isDefault: false,
});
const testCase = "rollover2";
// , per entity and regular
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for feature item (per entity)`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let customer: Customer;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
beforeAll(async () => {
stripeCli = ctx.stripeCli;
await initProductsV0({
ctx,
products: [free],
prefix: testCase,
customerId,
});
const res = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = res.testClockId!;
customer = res.customer;
});
const entities: any[] = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
];
test("should attach pro product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: free.id,
});
await autumn.entities.create(customerId, entities);
});
const entity1Id = entities[0].id;
const entity2Id = entities[1].id;
const newEntity1Balance = 300;
const newEntity2Balance = 200;
const includedUsage = msgesItem.included_usage;
const usages = [
{
entityId: entity1Id,
usage: includedUsage - newEntity1Balance,
rollover: newEntity1Balance,
},
{
entityId: entity2Id,
usage: includedUsage - newEntity2Balance,
rollover: newEntity2Balance,
},
];
test("should create track messages, reset, and have correct rollover", async () => {
for (const usage of usages) {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: usage.usage,
entity_id: usage.entityId,
});
}
await timeout(3000);
// Run reset cusEnt on ...
await resetAndGetCusEnt({
db: ctx.db,
customer,
productGroup: free.group,
featureId: TestFeature.Messages,
});
for (const usage of usages) {
const entity = await autumn.entities.get(customerId, usage.entityId);
const msgesFeature = entity.features[TestFeature.Messages];
const expectedRollover = Math.min(usage.rollover, rolloverConfig.max);
expect(msgesFeature.rollovers.length).toBe(1);
expect(msgesFeature.balance).toBe(includedUsage + expectedRollover);
expect(msgesFeature.rollovers[0].balance).toBe(expectedRollover);
}
});
test("should reset again and have correct rollovers", async () => {
await resetAndGetCusEnt({
db: ctx.db,
customer,
productGroup: free.group,
featureId: TestFeature.Messages,
});
const entity1 = await autumn.entities.get(customerId, entity1Id);
const entity1Msges = entity1.features[TestFeature.Messages];
// 400, 300 -> 400, 100 (max is 500)
const rollovers = entity1Msges.rollovers;
expect(rollovers[0].balance).toBe(100);
expect(rollovers[1].balance).toBe(400);
const entity2 = await autumn.entities.get(customerId, entity2Id);
const entity2Msges = entity2.features[TestFeature.Messages];
// 400, 200 -> 400, 0 (max is 500)
const rollovers2 = entity2Msges.rollovers;
expect(rollovers2[0].balance).toBe(100);
expect(rollovers2[1].balance).toBe(400);
});
test("should track and deduct from oldest rollovers first", async () => {
for (const entity of entities) {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: 150,
entity_id: entity.id,
});
await timeout(2000);
const entRes = await autumn.entities.get(customerId, entity.id);
const msgesFeature = entRes.features[TestFeature.Messages];
const rollovers = msgesFeature.rollovers;
expect(rollovers[0].balance).toBe(0);
expect(rollovers[1].balance).toBe(350);
expect(msgesFeature.balance).toBe(includedUsage + 350);
}
});
test("should track past rollovers and deduct from original balance", async () => {
for (const entity of entities) {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: 400,
entity_id: entity.id,
});
await timeout(2000);
const entRes = await autumn.entities.get(customerId, entity.id);
const msgesFeature = entRes.features[TestFeature.Messages];
const rollovers = msgesFeature.rollovers;
expect(rollovers[0].balance).toBe(0);
expect(rollovers[1].balance).toBe(0);
expect(msgesFeature.balance).toBe(includedUsage - 50);
}
});
});

View File

@@ -0,0 +1,127 @@
import {
type AppEnv,
type Customer,
LegacyVersion,
type LimitedItem,
type Organization,
RolloverDuration,
} from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import { addMonths } from "date-fns";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
const rolloverConfig = {
max: 500,
length: 1,
duration: RolloverDuration.Month,
};
const messagesItem = constructArrearProratedItem({
featureId: TestFeature.Messages,
includedUsage: 400,
rolloverConfig,
}) as LimitedItem;
export const pro = constructProduct({
items: [messagesItem],
type: "pro",
isDefault: false,
});
const testCase = "rollover3";
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price feature`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let customer: Customer;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [pro],
prefix: testCase,
});
await createProducts({
autumn,
products: [pro],
customerId,
db,
orgId: org.id,
env,
});
const res = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = res.testClockId!;
customer = res.customer;
});
it("should attach pro product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
});
const rollover = 250;
let curBalance = messagesItem.included_usage;
it("should create track messages, reset, and have correct rollover", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: messagesItem.included_usage - rollover,
});
await timeout(3000);
await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addMonths(new Date(), 1).getTime(),
waitForSeconds: 20,
});
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
const expectedBalance = messagesItem.included_usage + rollover;
expect(msgesFeature).to.exist;
expect(msgesFeature?.balance).to.equal(expectedBalance);
// @ts-expect-error
expect(msgesFeature?.rollovers[0].balance).to.equal(rollover);
curBalance = expectedBalance;
});
});

View File

@@ -0,0 +1,108 @@
import {
type Customer,
LegacyVersion,
type LimitedItem,
RolloverDuration,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addMonths } from "date-fns";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
const rolloverConfig = {
max: 500,
length: 1,
duration: RolloverDuration.Month,
};
const messagesItem = constructArrearProratedItem({
featureId: TestFeature.Messages,
includedUsage: 400,
rolloverConfig,
}) as LimitedItem;
export const pro = constructProduct({
items: [messagesItem],
type: "pro",
isDefault: false,
});
const testCase = "rollover3";
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price feature`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let customer: Customer;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
beforeAll(async () => {
stripeCli = ctx.stripeCli;
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
customerId,
});
const res = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = res.testClockId!;
customer = res.customer;
});
test("should attach pro product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
});
const rollover = 250;
let curBalance = messagesItem.included_usage;
test("should create track messages, reset, and have correct rollover", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: messagesItem.included_usage - rollover,
});
await timeout(3000);
await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addMonths(new Date(), 1).getTime(),
waitForSeconds: 20,
});
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
const expectedBalance = messagesItem.included_usage + rollover;
expect(msgesFeature).toBeDefined();
expect(msgesFeature?.balance).toBe(expectedBalance);
// @ts-expect-error
expect(msgesFeature?.rollovers[0].balance).toBe(rollover);
curBalance = expectedBalance;
});
});

View File

@@ -0,0 +1,157 @@
import {
type AppEnv,
type Customer,
LegacyVersion,
type LimitedItem,
type Organization,
RolloverDuration,
} from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import { addMonths } from "date-fns";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
const rolloverConfig = {
max: 400,
length: 1,
duration: RolloverDuration.Month,
};
const messagesItem = constructPrepaidItem({
featureId: TestFeature.Messages,
includedUsage: 100,
billingUnits: 300,
price: 10,
rolloverConfig,
}) as LimitedItem;
export const pro = constructProduct({
items: [messagesItem],
type: "pro",
isDefault: false,
});
const testCase = "rollover4";
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price feature`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let customer: Customer;
let stripeCli: Stripe;
let curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [pro],
prefix: testCase,
});
await createProducts({
autumn,
products: [pro],
customerId,
db,
orgId: org.id,
env,
});
const res = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = res.testClockId!;
customer = res.customer;
});
const paidQuantity = 300;
const balance = paidQuantity + messagesItem.included_usage;
const options = [
{
feature_id: TestFeature.Messages,
quantity: paidQuantity,
},
];
it("should attach pro product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
options,
});
});
const rollover = 50;
it("should create track messages, reset, and have correct rollover", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: balance - rollover,
});
await timeout(3000);
curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addMonths(new Date(), 1).getTime(),
waitForSeconds: 20,
});
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
// @ts-expect-error
const rollovers = msgesFeature?.rollovers;
expect(msgesFeature).to.exist;
expect(msgesFeature?.balance).to.equal(balance + rollover);
expect(rollovers[0].balance).to.equal(rollover);
});
// let usage2 = 50;
it("should reset again and have correct rollover", async () => {
await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addMonths(curUnix, 1).getTime(),
waitForSeconds: 20,
});
const newRollover = Math.min(balance + rollover, rolloverConfig.max);
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
// @ts-expect-error
const rollovers = msgesFeature?.rollovers;
expect(msgesFeature).to.exist;
expect(msgesFeature?.balance).to.equal(balance + newRollover);
expect(rollovers[0].balance).to.equal(0);
expect(rollovers[1].balance).to.equal(400);
});
});

View File

@@ -0,0 +1,138 @@
import {
type Customer,
LegacyVersion,
type LimitedItem,
RolloverDuration,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addMonths } from "date-fns";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
const rolloverConfig = {
max: 400,
length: 1,
duration: RolloverDuration.Month,
};
const messagesItem = constructPrepaidItem({
featureId: TestFeature.Messages,
includedUsage: 100,
billingUnits: 300,
price: 10,
rolloverConfig,
}) as LimitedItem;
export const pro = constructProduct({
items: [messagesItem],
type: "pro",
isDefault: false,
});
const testCase = "rollover4";
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for usage price feature`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let customer: Customer;
let stripeCli: Stripe;
let curUnix = new Date().getTime();
beforeAll(async () => {
stripeCli = ctx.stripeCli;
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
customerId,
});
const res = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = res.testClockId!;
customer = res.customer;
});
const paidQuantity = 300;
const balance = paidQuantity + messagesItem.included_usage;
const options = [
{
feature_id: TestFeature.Messages,
quantity: paidQuantity,
},
];
test("should attach pro product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
options,
});
});
const rollover = 50;
test("should create track messages, reset, and have correct rollover", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: balance - rollover,
});
await timeout(3000);
curUnix = await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addMonths(new Date(), 1).getTime(),
waitForSeconds: 20,
});
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
// @ts-expect-error
const rollovers = msgesFeature?.rollovers;
expect(msgesFeature).toBeDefined();
expect(msgesFeature?.balance).toBe(balance + rollover);
expect(rollovers[0].balance).toBe(rollover);
});
// let usage2 = 50;
test("should reset again and have correct rollover", async () => {
await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addMonths(curUnix, 1).getTime(),
waitForSeconds: 20,
});
const newRollover = Math.min(balance + rollover, rolloverConfig.max);
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
// @ts-expect-error
const rollovers = msgesFeature?.rollovers;
expect(msgesFeature).toBeDefined();
expect(msgesFeature?.balance).toBe(balance + newRollover);
expect(rollovers[0].balance).toBe(0);
expect(rollovers[1].balance).toBe(400);
});
});

View File

@@ -0,0 +1,137 @@
import {
type AppEnv,
type Customer,
LegacyVersion,
type LimitedItem,
type Organization,
RolloverDuration,
} from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { resetAndGetCusEnt } from "./rolloverTestUtils.js";
const freeRollover = { max: 1000, length: 1, duration: RolloverDuration.Month };
const proRollover = { max: 600, length: 1, duration: RolloverDuration.Month };
const freeMsges = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 500,
rolloverConfig: freeRollover,
}) as LimitedItem;
const proMsges = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 500,
rolloverConfig: proRollover,
}) as LimitedItem;
const free = constructProduct({
items: [freeMsges],
type: "free",
isDefault: false,
});
const pro = constructProduct({
items: [proMsges],
type: "pro",
});
const testCase = "rollover5";
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let customer: Customer;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [free, pro],
prefix: testCase,
});
await createProducts({
autumn,
products: [free, pro],
customerId,
db,
orgId: org.id,
env,
});
const res = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = res.testClockId!;
customer = res.customer;
});
it("should attach free product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: free.id,
});
});
it("should create rollovers", async () => {
await resetAndGetCusEnt({
customer,
db,
productGroup: testCase,
featureId: TestFeature.Messages,
});
await resetAndGetCusEnt({
customer,
db,
productGroup: testCase,
featureId: TestFeature.Messages,
});
// Attach pro
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
const freeRolloverBalance = freeMsges.included_usage * 2;
const proRolloverBalance = Math.min(proRollover.max, freeRolloverBalance);
expect(msgesFeature).to.exist;
expect(msgesFeature?.balance).to.equal(
proMsges.included_usage + proRolloverBalance,
);
// @ts-expect-error
const rollovers = msgesFeature?.rollovers;
expect(rollovers[0].balance).to.equal(100);
expect(rollovers[1].balance).to.equal(500);
});
});

View File

@@ -0,0 +1,118 @@
import {
type Customer,
LegacyVersion,
type LimitedItem,
RolloverDuration,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { resetAndGetCusEnt } from "./rolloverTestUtils.js";
const freeRollover = { max: 1000, length: 1, duration: RolloverDuration.Month };
const proRollover = { max: 600, length: 1, duration: RolloverDuration.Month };
const freeMsges = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 500,
rolloverConfig: freeRollover,
}) as LimitedItem;
const proMsges = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 500,
rolloverConfig: proRollover,
}) as LimitedItem;
const free = constructProduct({
items: [freeMsges],
type: "free",
isDefault: false,
});
const pro = constructProduct({
items: [proMsges],
type: "pro",
});
const testCase = "rollover5";
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let customer: Customer;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
beforeAll(async () => {
stripeCli = ctx.stripeCli;
await initProductsV0({
ctx,
products: [free, pro],
prefix: testCase,
customerId,
});
const res = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = res.testClockId!;
customer = res.customer;
});
test("should attach free product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: free.id,
});
});
test("should create rollovers", async () => {
await resetAndGetCusEnt({
customer,
db: ctx.db,
productGroup: testCase,
featureId: TestFeature.Messages,
});
await resetAndGetCusEnt({
customer,
db: ctx.db,
productGroup: testCase,
featureId: TestFeature.Messages,
});
// Attach pro
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
const freeRolloverBalance = freeMsges.included_usage * 2;
const proRolloverBalance = Math.min(proRollover.max, freeRolloverBalance);
expect(msgesFeature).toBeDefined();
expect(msgesFeature?.balance).toBe(
proMsges.included_usage + proRolloverBalance,
);
// @ts-expect-error
const rollovers = msgesFeature?.rollovers;
expect(rollovers[0].balance).toBe(100);
expect(rollovers[1].balance).toBe(500);
});
});

View File

@@ -0,0 +1,151 @@
import {
type AppEnv,
type Customer,
LegacyVersion,
type LimitedItem,
type Organization,
RolloverDuration,
} from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import { resetAndGetCusEnt } from "./rolloverTestUtils.js";
const freeRollover = { max: 600, length: 1, duration: RolloverDuration.Month };
const proRollover = { max: 1000, length: 1, duration: RolloverDuration.Month };
const freeMsges = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 500,
rolloverConfig: freeRollover,
}) as LimitedItem;
const proMsges = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 500,
rolloverConfig: proRollover,
}) as LimitedItem;
const free = constructProduct({
items: [freeMsges],
type: "free",
isDefault: false,
});
const pro = constructProduct({
items: [proMsges],
type: "pro",
});
const testCase = "rollover6";
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let customer: Customer;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [free, pro],
prefix: testCase,
});
await createProducts({
autumn,
products: [free, pro],
customerId,
db,
orgId: org.id,
env,
});
const res = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = res.testClockId!;
customer = res.customer;
});
it("should attach free product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
});
it("should create rollovers", async () => {
await resetAndGetCusEnt({
customer,
db,
productGroup: testCase,
featureId: TestFeature.Messages,
});
await resetAndGetCusEnt({
customer,
db,
productGroup: testCase,
featureId: TestFeature.Messages,
});
// Attach pro
await autumn.attach({
customer_id: customerId,
product_id: free.id,
});
await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addHours(
addMonths(curUnix, 1),
hoursToFinalizeInvoice,
).getTime(),
waitForSeconds: 20,
});
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
const proRolloverBalance = proMsges.included_usage * 2;
const freeRolloverBalance = Math.min(freeRollover.max, proRolloverBalance);
expect(msgesFeature).to.exist;
expect(msgesFeature?.balance).to.equal(
freeMsges.included_usage + freeRolloverBalance,
);
// @ts-expect-error
const rollovers = msgesFeature?.rollovers;
expect(rollovers[0].balance).to.equal(100);
expect(rollovers[1].balance).to.equal(500);
});
});

View File

@@ -0,0 +1,132 @@
import {
type Customer,
LegacyVersion,
type LimitedItem,
RolloverDuration,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { addHours, addMonths } from "date-fns";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { hoursToFinalizeInvoice } from "tests/utils/constants.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { advanceTestClock } from "@/utils/scriptUtils/testClockUtils.js";
import { resetAndGetCusEnt } from "./rolloverTestUtils.js";
const freeRollover = { max: 600, length: 1, duration: RolloverDuration.Month };
const proRollover = { max: 1000, length: 1, duration: RolloverDuration.Month };
const freeMsges = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 500,
rolloverConfig: freeRollover,
}) as LimitedItem;
const proMsges = constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 500,
rolloverConfig: proRollover,
}) as LimitedItem;
const free = constructProduct({
items: [freeMsges],
type: "free",
isDefault: false,
});
const pro = constructProduct({
items: [proMsges],
type: "pro",
});
const testCase = "rollover6";
describe(`${chalk.yellowBright(`${testCase}: Testing rollovers for upgrade`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let customer: Customer;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
beforeAll(async () => {
stripeCli = ctx.stripeCli;
await initProductsV0({
ctx,
products: [free, pro],
prefix: testCase,
customerId,
});
const res = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = res.testClockId!;
customer = res.customer;
});
test("should attach free product", async () => {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
});
});
test("should create rollovers", async () => {
await resetAndGetCusEnt({
customer,
db: ctx.db,
productGroup: testCase,
featureId: TestFeature.Messages,
});
await resetAndGetCusEnt({
customer,
db: ctx.db,
productGroup: testCase,
featureId: TestFeature.Messages,
});
// Attach pro
await autumn.attach({
customer_id: customerId,
product_id: free.id,
});
await advanceTestClock({
stripeCli,
testClockId,
advanceTo: addHours(
addMonths(curUnix, 1),
hoursToFinalizeInvoice,
).getTime(),
waitForSeconds: 20,
});
const cus = await autumn.customers.get(customerId);
const msgesFeature = cus.features[TestFeature.Messages];
const proRolloverBalance = proMsges.included_usage * 2;
const freeRolloverBalance = Math.min(freeRollover.max, proRolloverBalance);
expect(msgesFeature).toBeDefined();
expect(msgesFeature?.balance).toBe(
freeMsges.included_usage + freeRolloverBalance,
);
// @ts-expect-error
const rollovers = msgesFeature?.rollovers;
expect(rollovers[0].balance).toBe(100);
expect(rollovers[1].balance).toBe(500);
});
});

View File

@@ -0,0 +1,45 @@
import { BillingInterval, ProductItemInterval } from "@autumn/shared";
import { TestFeature } from "tests/setup/v2Features.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { constructPriceItem } from "@/internal/products/product-items/productItemUtils.js";
import {
constructFeatureItem,
constructArrearItem,
} from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { createSharedProducts } from "@/utils/scriptUtils/testUtils/createSharedProduct.js";
/**
* Shared products for usage test group
* Matches global products.proWithOverage
*/
export const sharedProWithOverage = constructProduct({
id: "pro-with-overage",
type: "pro",
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 10,
interval: ProductItemInterval.Month,
}),
constructPriceItem({
price: 2000, // $20/month
interval: BillingInterval.Month,
}),
constructArrearItem({
featureId: TestFeature.Messages,
// Overage pricing for usage beyond included
}),
],
});
export const initUsageSharedProducts = async () => {
await createSharedProducts({
ctx,
products: [sharedProWithOverage],
});
};
// Auto-init on import (backwards compat)
await initUsageSharedProducts();

View File

@@ -0,0 +1,139 @@
import type { Customer } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import { v1ProductToBasePrice } from "tests/utils/testProductUtils/testProductUtils.js";
import { calculateMetered1Price } from "@/external/stripe/utils.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { AutumnCli } from "../../cli/AutumnCli.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
import { timeout } from "../../utils/genUtils.js";
import { advanceClockForInvoice } from "../../utils/stripeUtils.js";
import { sharedProWithOverage } from "./sharedProducts.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { convertProductV2ToV1 } from "@/internal/products/productUtils/productV2Utils/convertProductV2ToV1.js";
const testCase = "usage1";
describe(`${chalk.yellowBright("usage1: Testing basic usage product")}`, () => {
const NUM_EVENTS = 50;
const customerId = testCase;
let testClockId: string;
let customer: Customer;
let stripeCli: Stripe;
beforeAll(async () => {
stripeCli = ctx.stripeCli;
const { customer: customer_, testClockId: testClockId_ } =
await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" },
withTestClock: true,
attachPm: "success",
});
customer = customer_;
testClockId = testClockId_;
});
test("should attach usage based product", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: sharedProWithOverage.id,
});
const res = await AutumnCli.getCustomer(customerId);
expectCustomerV0Correct({
sent: sharedProWithOverage,
cusRes: res,
ctx,
});
});
test("usage1: should send metered1 events", async () => {
const batchUpdates = [];
for (let i = 0; i < NUM_EVENTS; i++) {
batchUpdates.push(
AutumnCli.sendEvent({
customerId: customerId,
eventName: TestFeature.Messages,
}),
);
}
await Promise.all(batchUpdates);
await timeout(25000);
});
test("should have correct metered1 balance after sending events", async () => {
const res: any = await AutumnCli.entitled(customerId, TestFeature.Messages);
expect(res!.allowed).toBe(true);
const balance = res!.balances.find(
(balance: any) => balance.feature_id === TestFeature.Messages,
);
// Convert V2 product to V1 to access entitlements
const productV1 = convertProductV2ToV1({
productV2: sharedProWithOverage,
orgId: ctx.org.id,
features: ctx.features,
});
const proOverageAmt =
productV1.entitlements.messages.allowance;
expect(res!.allowed, "should be allowed").toBe(true);
expect(balance?.balance, "should have correct metered1 balance").toBe(
proOverageAmt! - NUM_EVENTS,
);
expect(balance?.usage_allowed, "should have usage_allowed").toBe(true);
});
// Check invoice
test("should advance stripe test clock and wait for event", async () => {
await advanceClockForInvoice({
stripeCli,
testClockId,
waitForMeterUpdate: true,
});
});
test("should have correct invoice amount", async () => {
const cusRes = await AutumnCli.getCustomer(customerId);
const invoices = cusRes!.invoices;
// Convert V2 product to V1 for price calculations
const productV1 = convertProductV2ToV1({
productV2: sharedProWithOverage,
orgId: ctx.org.id,
features: ctx.features,
});
// calculate price
const price = calculateMetered1Price({
product: productV1,
numEvents: NUM_EVENTS,
metered1Feature: ctx.features[TestFeature.Messages],
});
expect(invoices.length).toBe(2);
const invoice = invoices[0];
const basePrice = v1ProductToBasePrice({
prices: productV1.prices,
});
expect(invoice.total, "invoice total should be usage price + base price").toBe(
price + basePrice,
);
});
});

View File

@@ -0,0 +1,116 @@
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import { Decimal } from "decimal.js";
import type Stripe from "stripe";
import { advanceClockForInvoice } from "tests/utils/stripeUtils.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { AutumnCli } from "../../cli/AutumnCli.js";
import { advanceProducts, creditSystems, features } from "../../global.js";
import { getCreditsUsed } from "../../utils/advancedUsageUtils.js";
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
import { timeout } from "../../utils/genUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
// NOTE: This test uses GPU products from global.ts (advanceProducts.gpuSystemStarter)
// These products are not yet converted to ProductV2 format in sharedProducts.ts
// The test has been migrated to Bun but still uses ProductV1 from global.ts
const testCase = "usage2";
describe(`${chalk.yellowBright("usage2: Testing basic usage product")}`, () => {
const customerId = testCase;
const PRECISION = 10;
const ASSERT_INVOICE_AMOUNT = true;
const CREDIT_MULTIPLIER = 100000;
let testClockId = "";
let totalCreditsUsed = 0;
let stripeCli: Stripe;
beforeAll(async () => {
const { testClockId: createdTestClockId } = await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" },
withTestClock: true,
attachPm: "success",
});
testClockId = createdTestClockId;
stripeCli = ctx.stripeCli;
});
test("should attach gpu system starter", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: advanceProducts.gpuSystemStarter.id,
});
const res = await AutumnCli.getCustomer(customerId);
expectCustomerV0Correct({
sent: advanceProducts.gpuSystemStarter,
cusRes: res,
ctx,
});
});
// Use up events
test("should send events and have correct balance (up to 10 DP)", async () => {
const eventCount = 20;
const batchEvents = [];
for (let i = 0; i < eventCount; i++) {
const randomVal = new Decimal(Math.random().toFixed(PRECISION))
.mul(CREDIT_MULTIPLIER)
.mul(Math.random() > 0.2 ? 1 : -1)
.toNumber();
const gpuId = i % 2 === 0 ? features.gpu1.id : features.gpu2.id;
const creditsUsed = getCreditsUsed(
creditSystems.gpuCredits,
gpuId,
randomVal,
);
totalCreditsUsed = new Decimal(totalCreditsUsed)
.plus(creditsUsed)
.toNumber();
batchEvents.push(
AutumnCli.sendEvent({
customerId: customerId,
eventName: gpuId,
properties: { value: randomVal },
}),
);
}
await Promise.all(batchEvents);
await timeout(10000);
const { allowed, balanceObj }: any = await AutumnCli.entitled(
customerId,
creditSystems.gpuCredits.id,
true,
);
const creditAllowance =
advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!;
expect(allowed).toBe(true);
expect(balanceObj!.balance).toBe(
new Decimal(creditAllowance).minus(totalCreditsUsed).toNumber(),
);
});
// Check invoice.created event
test("should have correct invoice amount / updated meter balance", async () => {
await advanceClockForInvoice({
stripeCli,
testClockId,
waitForMeterUpdate: ASSERT_INVOICE_AMOUNT,
});
});
});

View File

@@ -0,0 +1,144 @@
import chalk from "chalk";
import { advanceProducts } from "../../global.js";
import { AutumnCli } from "../../cli/AutumnCli.js";
import { sendGPUEvents } from "../../utils/advancedUsageUtils.js";
import { advanceTestClock } from "../../utils/stripeUtils.js";
import { expect } from "bun:test";
import { Decimal } from "decimal.js";
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
import { checkSubscriptionContainsProducts } from "tests/utils/scheduleCheckUtils.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { beforeAll, describe, test } from "bun:test";
import Stripe from "stripe";
import { priceToInvoiceAmount } from "@/internal/products/prices/priceUtils/priceToInvoiceAmount.js";
import { calculateProrationAmount } from "@/internal/invoices/prorationUtils.js";
import { getSubsFromCusId } from "tests/utils/expectUtils/expectSubUtils.js";
import { subToPeriodStartEnd } from "@/external/stripe/stripeSubUtils/convertSubUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
// NOTE: This test uses GPU products from global.ts (advanceProducts.gpuSystemStarter, gpuSystemPro)
// These products are not yet converted to ProductV2 format in sharedProducts.ts
// The test has been migrated to Bun but still uses ProductV1 from global.ts
const testCase = "usage3";
const ASSERT_INVOICE_AMOUNT = true;
describe(`${chalk.yellowBright(
"usage3: upgrade from GPU starter monthly to GPU pro monthly",
)}`, () => {
const customerId = "usage3";
let testClockId = "";
let totalCreditsUsed = 0;
let stripeCli: Stripe;
let curUnix = 0;
beforeAll(async () => {
let { testClockId: insertedTestClockId } = await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" },
withTestClock: true,
attachPm: "success",
});
testClockId = insertedTestClockId;
stripeCli = ctx.stripeCli;
});
// 1. Attach GPU starter monthly
test("usage3: should attach GPU starter monthly", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: advanceProducts.gpuSystemStarter.id,
});
});
// 2. Send 20 events
test("usage3: should send 20 events", async () => {
let eventCount = 20;
const { creditsUsed } = await sendGPUEvents({
customerId,
eventCount,
});
totalCreditsUsed = creditsUsed;
});
// 3. Advance test clock by 15 days and upgrade
test("should advance test clock by 15 days and upgrade to GPU pro monthly", async () => {
curUnix = await advanceTestClock({
stripeCli,
testClockId,
numberOfDays: 15,
});
await AutumnCli.attach({
customerId: customerId,
productId: advanceProducts.gpuSystemPro.id,
});
// MAKE SURE STRIPE SUB ONLY HAS GPU PRO
const res = await AutumnCli.getCustomer(customerId);
expectCustomerV0Correct({
sent: advanceProducts.gpuSystemPro,
cusRes: res,
ctx,
});
let subscriptionId = res.products[0].subscription_ids![0]!;
await checkSubscriptionContainsProducts({
db: ctx.db,
org: ctx.org,
env: ctx.env,
subscriptionId,
productIds: [advanceProducts.gpuSystemPro.id],
});
});
// 4. Check invoice for 15 days of starter usage
test("should have invoice for 15 days of starter usage", async () => {
const res = await AutumnCli.getCustomer(customerId);
const invoices = res!.invoices;
let basePrice1 = advanceProducts.gpuSystemStarter.prices[0].config.amount;
let basePrice2 = advanceProducts.gpuSystemPro.prices[0].config.amount;
let { subs } = await getSubsFromCusId({
db: ctx.db,
org: ctx.org,
env: ctx.env,
customerId,
stripeCli,
productId: advanceProducts.gpuSystemPro.id,
});
let sub = subs[0];
const { start, end } = subToPeriodStartEnd({ sub });
let baseDiff = calculateProrationAmount({
periodStart: start * 1000,
periodEnd: end * 1000,
now: curUnix,
amount: basePrice2 - basePrice1,
allowNegative: true,
});
let usagePrice = advanceProducts.gpuSystemStarter.prices[1];
let overage =
totalCreditsUsed -
advanceProducts.gpuSystemStarter.entitlements.gpuCredits.allowance!;
let overagePrice = priceToInvoiceAmount({
price: usagePrice,
overage,
});
let calculatedTotal = new Decimal(baseDiff)
.plus(overagePrice)
.toDecimalPlaces(2)
.toNumber();
expect(invoices[0].total).toBe(calculatedTotal);
});
});

View File

@@ -0,0 +1,112 @@
import type { Customer } from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { AutumnCli } from "../../cli/AutumnCli.js";
import { advanceProducts, creditSystems } from "../../global.js";
import {
checkCreditBalance,
checkUsageInvoiceAmount,
sendGPUEvents,
} from "../../utils/advancedUsageUtils.js";
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
import { advanceClockForInvoice } from "../../utils/stripeUtils.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
// NOTE: This test uses GPU products from global.ts (advanceProducts.gpuStarterAnnual)
// These products are not yet converted to ProductV2 format in sharedProducts.ts
// The test has been migrated to Bun but still uses ProductV1 from global.ts
// However, it does use checkUsageInvoiceAmountV2 for the V2 helper function
const testCase = "usage4";
describe(`${chalk.yellowBright("usage4: GPU starter annual")}`, () => {
const customerId = testCase;
let totalCreditsUsed = 0;
let testClockId = "";
let customer: Customer;
let stripeCli: Stripe;
beforeAll(async () => {
const res = await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" },
withTestClock: true,
attachPm: "success",
});
testClockId = res.testClockId;
customer = res.customer;
stripeCli = ctx.stripeCli;
});
test("should attach GPU starter annual", async () => {
await AutumnCli.attach({
customerId: customerId,
productId: advanceProducts.gpuStarterAnnual.id,
});
const res = await AutumnCli.getCustomer(customerId);
expectCustomerV0Correct({
sent: advanceProducts.gpuStarterAnnual,
cusRes: res,
ctx,
});
expect(res!.invoices.length).toBe(1);
});
test("should send 20 events and have correct balance", async () => {
const eventCount = 20;
const { creditsUsed } = await sendGPUEvents({
customerId,
eventCount,
});
totalCreditsUsed = creditsUsed;
await checkCreditBalance({
customerId,
featureId: creditSystems.gpuCredits.id,
totalCreditsUsed,
originalAllowance:
advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!,
});
});
test("should have invoice after a month and correct balance", async () => {
await advanceClockForInvoice({
stripeCli,
testClockId,
waitForMeterUpdate: true,
});
const res = await AutumnCli.getCustomer(customerId);
const invoices = res!.invoices;
const invoiceIndex = invoices.findIndex((invoice: any) =>
invoice.product_ids.includes(advanceProducts.gpuStarterAnnual.id),
);
// NOTE: Using checkUsageInvoiceAmount (V1) as gpuStarterAnnual is not yet converted to V2
// When GPU products are migrated to ProductV2, this should use checkUsageInvoiceAmountV2
await checkUsageInvoiceAmount({
invoices,
totalUsage: totalCreditsUsed,
product: advanceProducts.gpuStarterAnnual,
featureId: creditSystems.gpuCredits.id,
invoiceIndex,
includeBase: false,
});
await checkCreditBalance({
customerId,
featureId: creditSystems.gpuCredits.id,
totalCreditsUsed: 0,
originalAllowance:
advanceProducts.gpuStarterAnnual.entitlements.gpuCredits.allowance!,
});
});
});

View File

@@ -0,0 +1,151 @@
import {
type AppEnv,
ErrCode,
LegacyVersion,
type Organization,
} from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
const userItem = constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 50,
includedUsage: 0,
usageLimit: 2,
});
export const pro = constructProduct({
items: [userItem],
type: "pro",
});
const testCase = "usageLimit1";
describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for entities`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [pro],
prefix: testCase,
});
await createProducts({
autumn,
products: [pro],
customerId,
db,
orgId: org.id,
env,
});
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = testClockId1!;
});
const entities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
{
id: "3",
name: "Entity 3",
feature_id: TestFeature.Users,
},
{
id: "4",
name: "Entity 4",
feature_id: TestFeature.Users,
},
];
it("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
});
});
it("should create more entities than the limit and hit error", async () => {
await expectAutumnError({
errCode: ErrCode.FeatureLimitReached,
func: async () => {
await autumn.entities.create(customerId, entities);
},
});
});
it("should create entities one by one, then hit usage limit", async () => {
await autumn.entities.create(customerId, entities[0]);
await autumn.entities.create(customerId, entities[1]);
await expectAutumnError({
errCode: ErrCode.FeatureLimitReached,
func: async () => {
await autumn.entities.create(customerId, entities[2]);
},
});
});
it("should have correct check and get customer value", async () => {
const check = await autumn.check({
customer_id: customerId,
feature_id: TestFeature.Users,
});
const customer = await autumn.customers.get(customerId);
expect(check.balance).to.equal(-2);
// @ts-expect-error
expect(check.usage_limit).to.equal(userItem.usage_limit);
// @ts-expect-error
expect(customer.features[TestFeature.Users].usage_limit).to.equal(
userItem.usage_limit,
);
});
});

View File

@@ -0,0 +1,132 @@
import {
ErrCode,
LegacyVersion,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const userItem = constructArrearProratedItem({
featureId: TestFeature.Users,
pricePerUnit: 50,
includedUsage: 0,
usageLimit: 2,
});
export const pro = constructProduct({
items: [userItem],
type: "pro",
});
const testCase = "usageLimit1";
describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for entities`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
beforeAll(async () => {
stripeCli = ctx.stripeCli;
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
customerId,
});
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = testClockId1!;
});
const entities = [
{
id: "1",
name: "Entity 1",
feature_id: TestFeature.Users,
},
{
id: "2",
name: "Entity 2",
feature_id: TestFeature.Users,
},
{
id: "3",
name: "Entity 3",
feature_id: TestFeature.Users,
},
{
id: "4",
name: "Entity 4",
feature_id: TestFeature.Users,
},
];
test("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
test("should create more entities than the limit and hit error", async () => {
await expectAutumnError({
errCode: ErrCode.FeatureLimitReached,
func: async () => {
await autumn.entities.create(customerId, entities);
},
});
});
test("should create entities one by one, then hit usage limit", async () => {
await autumn.entities.create(customerId, entities[0]);
await autumn.entities.create(customerId, entities[1]);
await expectAutumnError({
errCode: ErrCode.FeatureLimitReached,
func: async () => {
await autumn.entities.create(customerId, entities[2]);
},
});
});
test("should have correct check and get customer value", async () => {
const check = await autumn.check({
customer_id: customerId,
feature_id: TestFeature.Users,
});
const customer = await autumn.customers.get(customerId);
expect(check.balance).toBe(-2);
// @ts-expect-error
expect(check.usage_limit).toBe(userItem.usage_limit);
// @ts-expect-error
expect(customer.features[TestFeature.Users].usage_limit).toBe(
userItem.usage_limit,
);
});
});

View File

@@ -0,0 +1,195 @@
import {
type AppEnv,
LegacyVersion,
type LimitedItem,
type Organization,
} from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import {
constructArrearItem,
constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
const messageItem = constructArrearItem({
featureId: TestFeature.Messages,
includedUsage: 100,
billingUnits: 1,
price: 0.5,
usageLimit: 500,
}) as LimitedItem;
export const pro = constructProduct({
items: [messageItem],
type: "pro",
});
const addOnMessages = constructFeatureItem({
featureId: TestFeature.Messages,
interval: null,
includedUsage: 250,
}) as LimitedItem;
const messageAddOn = constructProduct({
type: "one_off",
items: [addOnMessages],
});
const testCase = "usageLimit2";
describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [pro, messageAddOn],
prefix: testCase,
});
await createProducts({
autumn,
products: [pro, messageAddOn],
customerId,
db,
orgId: org.id,
env,
});
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = testClockId1!;
});
it("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
});
});
const initialUsage =
messageItem.included_usage + messageItem.usage_limit! + 1000;
it("should track more messages than limit and not surpass", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: initialUsage,
});
await timeout(2000);
const check = await autumn.check({
customer_id: customerId,
feature_id: TestFeature.Messages,
});
const customer = await autumn.customers.get(customerId);
const expectedBalance =
messageItem.included_usage - messageItem.usage_limit!;
expect(check.balance).to.equal(expectedBalance);
expect(check.allowed).to.equal(false);
// @ts-expect-error
expect(check.usage_limit!).to.equal(messageItem.usage_limit!);
// @ts-expect-error
expect(customer.features[TestFeature.Messages].usage_limit).to.equal(
messageItem.usage_limit!,
);
});
it("should purchase add ons and have correct check results", async () => {
await autumn.attach({
customer_id: customerId,
product_id: messageAddOn.id,
});
const check = await autumn.check({
customer_id: customerId,
feature_id: TestFeature.Messages,
});
const customer = await autumn.customers.get(customerId);
const expectedBalance =
messageItem.included_usage -
messageItem.usage_limit! +
addOnMessages.included_usage;
expect(check.balance).to.equal(expectedBalance);
expect(check.allowed).to.equal(true);
// @ts-expect-error
expect(check.usage_limit!).to.equal(
messageItem.usage_limit! + addOnMessages.included_usage,
);
// @ts-expect-error
expect(customer.features[TestFeature.Messages].usage_limit).to.equal(
messageItem.usage_limit! + addOnMessages.included_usage,
);
});
it("should use up all add ons and have correct check results", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: addOnMessages.included_usage + 500,
});
await timeout(2000);
const check = await autumn.check({
customer_id: customerId,
feature_id: TestFeature.Messages,
});
const customer = await autumn.customers.get(customerId);
const expectedBalance =
messageItem.included_usage - messageItem.usage_limit!;
expect(check.balance).to.equal(expectedBalance);
expect(check.allowed).to.equal(false);
// @ts-expect-error
expect(check.usage_limit!).to.equal(
messageItem.usage_limit! + addOnMessages.included_usage,
);
// @ts-expect-error
expect(customer.features[TestFeature.Messages].usage_limit).to.equal(
messageItem.usage_limit! + addOnMessages.included_usage,
);
});
});

View File

@@ -0,0 +1,176 @@
import {
LegacyVersion,
type LimitedItem,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { timeout } from "@/utils/genUtils.js";
import {
constructArrearItem,
constructFeatureItem,
} from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const messageItem = constructArrearItem({
featureId: TestFeature.Messages,
includedUsage: 100,
billingUnits: 1,
price: 0.5,
usageLimit: 500,
}) as LimitedItem;
export const pro = constructProduct({
items: [messageItem],
type: "pro",
});
const addOnMessages = constructFeatureItem({
featureId: TestFeature.Messages,
interval: null,
includedUsage: 250,
}) as LimitedItem;
const messageAddOn = constructProduct({
type: "one_off",
items: [addOnMessages],
});
const testCase = "usageLimit2";
describe(`${chalk.yellowBright(`${testCase}: Testing usage limits, usage prices`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
beforeAll(async () => {
stripeCli = ctx.stripeCli;
await initProductsV0({
ctx,
products: [pro, messageAddOn],
prefix: testCase,
customerId,
});
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = testClockId1!;
});
test("should attach pro product", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
const initialUsage =
messageItem.included_usage + messageItem.usage_limit! + 1000;
test("should track more messages than limit and not surpass", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: initialUsage,
});
await timeout(2000);
const check = await autumn.check({
customer_id: customerId,
feature_id: TestFeature.Messages,
});
const customer = await autumn.customers.get(customerId);
const expectedBalance =
messageItem.included_usage - messageItem.usage_limit!;
expect(check.balance).toBe(expectedBalance);
expect(check.allowed).toBe(false);
// @ts-expect-error
expect(check.usage_limit!).toBe(messageItem.usage_limit!);
// @ts-expect-error
expect(customer.features[TestFeature.Messages].usage_limit).toBe(
messageItem.usage_limit!,
);
});
test("should purchase add ons and have correct check results", async () => {
await autumn.attach({
customer_id: customerId,
product_id: messageAddOn.id,
});
const check = await autumn.check({
customer_id: customerId,
feature_id: TestFeature.Messages,
});
const customer = await autumn.customers.get(customerId);
const expectedBalance =
messageItem.included_usage -
messageItem.usage_limit! +
addOnMessages.included_usage;
expect(check.balance).toBe(expectedBalance);
expect(check.allowed).toBe(true);
// @ts-expect-error
expect(check.usage_limit!).toBe(
messageItem.usage_limit! + addOnMessages.included_usage,
);
// @ts-expect-error
expect(customer.features[TestFeature.Messages].usage_limit).toBe(
messageItem.usage_limit! + addOnMessages.included_usage,
);
});
test("should use up all add ons and have correct check results", async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Messages,
value: addOnMessages.included_usage + 500,
});
await timeout(2000);
const check = await autumn.check({
customer_id: customerId,
feature_id: TestFeature.Messages,
});
const customer = await autumn.customers.get(customerId);
const expectedBalance =
messageItem.included_usage - messageItem.usage_limit!;
expect(check.balance).toBe(expectedBalance);
expect(check.allowed).toBe(false);
// @ts-expect-error
expect(check.usage_limit!).toBe(
messageItem.usage_limit! + addOnMessages.included_usage,
);
// @ts-expect-error
expect(customer.features[TestFeature.Messages].usage_limit).toBe(
messageItem.usage_limit! + addOnMessages.included_usage,
);
});
});

View File

@@ -0,0 +1,147 @@
import {
type AppEnv,
ErrCode,
LegacyVersion,
type LimitedItem,
type Organization,
} from "@autumn/shared";
import chalk from "chalk";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
const messageItem = constructPrepaidItem({
featureId: TestFeature.Messages,
includedUsage: 50,
billingUnits: 100,
price: 8,
usageLimit: 500,
}) as LimitedItem;
export const pro = constructProduct({
items: [messageItem],
type: "pro",
});
// const addOnMessages = constructFeatureItem({
// featureId: TestFeature.Messages,
// interval: null,
// includedUsage: 250,
// }) as LimitedItem;
// const messageAddOn = constructProduct({
// type: "one_off",
// items: [addOnMessages],
// });
const testCase = "usageLimit3";
describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for prepaid`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [pro],
prefix: testCase,
});
await createProducts({
autumn,
products: [pro],
customerId,
db,
orgId: org.id,
env,
});
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = testClockId1!;
});
it("should attach pro product with quantity exceeding usage limit and get an error", async () => {
expectAutumnError({
errCode: ErrCode.InvalidOptions,
func: async () => {
return await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
options: [
{
feature_id: TestFeature.Messages,
quantity: 600,
},
],
});
},
});
});
it("should attach pro product and update quantity with quantity exceeding usage limit and get an error", async () => {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
options: [
{
feature_id: TestFeature.Messages,
quantity: 100,
},
],
});
expectAutumnError({
errCode: ErrCode.InvalidOptions,
func: async () => {
return await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
options: [
{
feature_id: TestFeature.Messages,
quantity: 600,
},
],
});
},
});
});
});

View File

@@ -0,0 +1,129 @@
import {
ErrCode,
LegacyVersion,
type LimitedItem,
} from "@autumn/shared";
import { beforeAll, describe, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructPrepaidItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const messageItem = constructPrepaidItem({
featureId: TestFeature.Messages,
includedUsage: 50,
billingUnits: 100,
price: 8,
usageLimit: 500,
}) as LimitedItem;
export const pro = constructProduct({
items: [messageItem],
type: "pro",
});
// const addOnMessages = constructFeatureItem({
// featureId: TestFeature.Messages,
// interval: null,
// includedUsage: 250,
// }) as LimitedItem;
// const messageAddOn = constructProduct({
// type: "one_off",
// items: [addOnMessages],
// });
const testCase = "usageLimit3";
describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for prepaid`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
beforeAll(async () => {
stripeCli = ctx.stripeCli;
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
customerId,
});
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = testClockId1!;
});
test("should attach pro product with quantity exceeding usage limit and get an error", async () => {
expectAutumnError({
errCode: ErrCode.InvalidOptions,
func: async () => {
return await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
options: [
{
feature_id: TestFeature.Messages,
quantity: 600,
},
],
});
},
});
});
test("should attach pro product and update quantity with quantity exceeding usage limit and get an error", async () => {
await autumn.attach({
customer_id: customerId,
product_id: pro.id,
options: [
{
feature_id: TestFeature.Messages,
quantity: 100,
},
],
});
expectAutumnError({
errCode: ErrCode.InvalidOptions,
func: async () => {
return await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
options: [
{
feature_id: TestFeature.Messages,
quantity: 600,
},
],
});
},
});
});
});

View File

@@ -0,0 +1,112 @@
import {
type AppEnv,
ErrCode,
LegacyVersion,
type LimitedItem,
type Organization,
} from "@autumn/shared";
import { expect } from "chai";
import chalk from "chalk";
import type Stripe from "stripe";
import { addPrefixToProducts } from "tests/attach/utils.js";
import { setupBefore } from "tests/before.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { createProducts } from "tests/utils/productUtils.js";
import type { DrizzleCli } from "@/db/initDrizzle.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomer } from "@/utils/scriptUtils/initCustomer.js";
const messageItem = constructArrearProratedItem({
featureId: TestFeature.Users,
includedUsage: 1,
pricePerUnit: 10,
usageLimit: 3,
}) as LimitedItem;
export const pro = constructProduct({
items: [messageItem],
type: "pro",
});
const testCase = "usageLimit4";
describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for cont use item`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let db: DrizzleCli, org: Organization, env: AppEnv;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
before(async function () {
await setupBefore(this);
const { autumnJs } = this;
db = this.db;
org = this.org;
env = this.env;
stripeCli = this.stripeCli;
addPrefixToProducts({
products: [pro],
prefix: testCase,
});
await createProducts({
autumn,
products: [pro],
customerId,
db,
orgId: org.id,
env,
});
const { testClockId: testClockId1 } = await initCustomer({
autumn: autumnJs,
customerId,
db,
org,
env,
attachPm: "success",
});
testClockId = testClockId1!;
});
it("should attach pro product with quantity exceeding usage limit and get an error", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli,
db,
org,
env,
});
});
it("should attach pro product and update quantity with quantity exceeding usage limit and get an error", async () => {
await expectAutumnError({
errCode: ErrCode.InvalidInputs,
func: async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Users,
value: messageItem.usage_limit! + 1,
});
},
});
const check = await autumn.check({
customer_id: customerId,
feature_id: TestFeature.Users,
});
expect(check.balance).to.equal(0);
expect(check.allowed).to.equal(true);
});
});

View File

@@ -0,0 +1,93 @@
import {
ErrCode,
LegacyVersion,
type LimitedItem,
} from "@autumn/shared";
import { beforeAll, describe, expect, test } from "bun:test";
import chalk from "chalk";
import type Stripe from "stripe";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { attachAndExpectCorrect } from "tests/utils/expectUtils/expectAttach.js";
import { expectAutumnError } from "tests/utils/expectUtils/expectErrUtils.js";
import { AutumnInt } from "@/external/autumn/autumnCli.js";
import { constructArrearProratedItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
const messageItem = constructArrearProratedItem({
featureId: TestFeature.Users,
includedUsage: 1,
pricePerUnit: 10,
usageLimit: 3,
}) as LimitedItem;
export const pro = constructProduct({
items: [messageItem],
type: "pro",
});
const testCase = "usageLimit4";
describe(`${chalk.yellowBright(`${testCase}: Testing usage limits for cont use item`)}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_4 });
let testClockId: string;
let stripeCli: Stripe;
const curUnix = new Date().getTime();
beforeAll(async () => {
stripeCli = ctx.stripeCli;
await initProductsV0({
ctx,
products: [pro],
prefix: testCase,
customerId,
});
const { testClockId: testClockId1 } = await initCustomerV3({
ctx,
customerId,
customerData: {},
attachPm: "success",
withTestClock: true,
});
testClockId = testClockId1!;
});
test("should attach pro product with quantity exceeding usage limit and get an error", async () => {
await attachAndExpectCorrect({
autumn,
customerId,
product: pro,
stripeCli: ctx.stripeCli,
db: ctx.db,
org: ctx.org,
env: ctx.env,
});
});
test("should attach pro product and update quantity with quantity exceeding usage limit and get an error", async () => {
await expectAutumnError({
errCode: ErrCode.InvalidInputs,
func: async () => {
await autumn.track({
customer_id: customerId,
feature_id: TestFeature.Users,
value: messageItem.usage_limit! + 1,
});
},
});
const check = await autumn.check({
customer_id: customerId,
feature_id: TestFeature.Users,
});
expect(check.balance).toBe(0);
expect(check.allowed).toBe(true);
});
});

View File

@@ -1,10 +1,9 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { LegacyVersion } from "@autumn/shared";
import { ApiVersion } from "@autumn/shared";
import chalk from "chalk";
import { AutumnCli } from "tests/cli/AutumnCli.js";
import { features, products } from "tests/global.js";
import { TestFeature } from "tests/setup/v2Features.js";
import { compareMainProduct } from "tests/utils/compare.js";
import { expectCustomerV0Correct } from "tests/utils/expectUtils/expectCustomerV0Correct.js";
import { expectFeaturesCorrect } from "tests/utils/expectUtils/expectFeaturesCorrect.js";
import { expectProductAttached } from "tests/utils/expectUtils/expectProductAttached.js";
import ctx from "tests/utils/testInitUtils/createTestContext.js";
@@ -13,91 +12,103 @@ import { constructFeatureItem } from "@/utils/scriptUtils/constructItem.js";
import { constructProduct } from "@/utils/scriptUtils/createTestProducts.js";
import { initCustomerV3 } from "@/utils/scriptUtils/testUtils/initCustomerV3.js";
import { initProductsV0 } from "@/utils/scriptUtils/testUtils/initProductsV0.js";
import { sharedDefaultFree } from "./sharedProducts.js";
const freeProd = constructProduct({
const free2 = constructProduct({
type: "free",
id: "free2",
isDefault: false,
items: [
constructFeatureItem({
featureId: TestFeature.Messages,
includedUsage: 1000,
}),
// constructFixedPrice({
// price: 0,
// }),
],
});
const testCase = "basic1";
const customerId = testCase;
describe(`${chalk.yellowBright("basic1: Testing attach free product")}`, () => {
const customerId = testCase;
const autumn: AutumnInt = new AutumnInt({ version: LegacyVersion.v1_2 });
describe(`${chalk.yellowBright("basic1: Testing attach free, default product")}`, () => {
const autumnV1 = new AutumnInt({
secretKey: ctx.orgSecretKey,
version: ApiVersion.V1_2,
});
beforeAll(async () => {
console.log(`[basic1] Using org: ${ctx.org.slug} (${ctx.org.id})`);
console.log("Customer ID: ", customerId);
// Create products FIRST so default product can be attached to customer
await initProductsV0({
ctx,
products: [free2],
prefix: testCase,
customerId,
});
// Then create customer (will auto-attach default product if exists)
await initCustomerV3({
ctx,
customerId,
customerData: { fingerprint: "test" },
withTestClock: false,
});
await initProductsV0({
ctx,
products: [freeProd],
prefix: testCase,
withDefault: true,
});
});
test("should create customer and have default free active", async () => {
const data = await AutumnCli.getCustomer(customerId);
compareMainProduct({
sent: products.free,
await expectCustomerV0Correct({
sent: sharedDefaultFree,
cusRes: data,
});
});
test("should have correct entitlements", async () => {
const expectedEntitlement = products.free.entitlements.metered1;
// Expected: 5 allowance for Messages feature
const entitled = (await AutumnCli.entitled(
customerId,
features.metered1.id,
TestFeature.Messages,
)) as any;
console.log("Entitled response:", entitled);
const metered1Balance = entitled.balances.find(
(balance: any) => balance.feature_id === features.metered1.id,
(balance: any) => balance.feature_id === TestFeature.Messages,
);
expect(entitled.allowed).toBe(true);
expect(metered1Balance).toBeDefined();
expect(metered1Balance.balance).toBe(expectedEntitlement.allowance);
expect(metered1Balance.balance).toBe(5);
expect(metered1Balance.unlimited).toBeUndefined();
});
test("should have correct boolean1 entitlement", async () => {
const entitled = await AutumnCli.entitled(customerId, features.boolean1.id);
// Dashboard feature is not included in freeProd, should be false
const entitled = await AutumnCli.entitled(
customerId,
TestFeature.Dashboard,
);
expect(entitled!.allowed).toBe(false);
});
test("should attach free (with $0 price) and force checkout and succeed", async () => {
await autumn.attach({
await autumnV1.attach({
customer_id: customerId,
product_id: freeProd.id,
product_id: free2.id,
force_checkout: true,
});
const customer = await autumn.customers.get(customerId);
const customer = await autumnV1.customers.get(customerId);
expectProductAttached({
customer,
product: freeProd,
product: free2,
});
expectFeaturesCorrect({
customer,
product: freeProd,
product: free2,
otherProducts: [sharedDefaultFree],
});
});
});

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