latest
This commit is contained in:
77
bun.lock
77
bun.lock
@@ -253,6 +253,18 @@
|
||||
"typescript-eslint": "^8.26.0",
|
||||
},
|
||||
},
|
||||
"packages/stripe-sync": {
|
||||
"name": "@autumn/stripe-sync",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@supabase/stripe-sync-engine": "^0.48.5",
|
||||
"pg": "^8.20.0",
|
||||
"stripe": "catalog:",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.20.0",
|
||||
},
|
||||
},
|
||||
"scripts": {
|
||||
"name": "@autumn/scripts",
|
||||
"version": "1.0.0",
|
||||
@@ -282,6 +294,7 @@
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@autumn/ksuid": "workspace:*",
|
||||
"@autumn/shared": "workspace:*",
|
||||
"@autumn/stripe-sync": "workspace:*",
|
||||
"@aws-sdk/client-s3": "^3.1017.0",
|
||||
"@aws-sdk/client-scheduler": "^3.1004.0",
|
||||
"@aws-sdk/client-sqs": "^3.958.0",
|
||||
@@ -604,6 +617,8 @@
|
||||
|
||||
"@autumn/shared": ["@autumn/shared@workspace:shared"],
|
||||
|
||||
"@autumn/stripe-sync": ["@autumn/stripe-sync@workspace:packages/stripe-sync"],
|
||||
|
||||
"@autumn/vite": ["@autumn/vite@workspace:vite"],
|
||||
|
||||
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
|
||||
@@ -1178,11 +1193,11 @@
|
||||
|
||||
"@mermaid-js/parser": ["@mermaid-js/parser@1.1.0", "", { "dependencies": { "langium": "^4.0.0" } }, "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw=="],
|
||||
|
||||
"@mintlify/cli": ["@mintlify/cli@4.0.1107", "", { "dependencies": { "@inquirer/prompts": "7.9.0", "@mintlify/common": "1.0.847", "@mintlify/link-rot": "3.0.1022", "@mintlify/prebuild": "1.0.989", "@mintlify/previewing": "4.0.1050", "@mintlify/validation": "0.1.662", "adm-zip": "0.5.16", "chalk": "5.2.0", "color": "4.2.3", "detect-port": "1.5.1", "front-matter": "4.0.2", "fs-extra": "11.2.0", "ink": "6.3.0", "inquirer": "12.3.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.2.0", "open": "^8.4.2", "openid-client": "^6.8.2", "posthog-node": "5.17.2", "react": "19.2.3", "semver": "7.7.2", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "^4.3.6" }, "optionalDependencies": { "keytar": "^7.9.0" }, "bin": { "mint": "bin/index.js", "mintlify": "bin/index.js" } }, "sha512-kOj8kFyebIzPkitgiCy7eWiJnCzPALOzEccQCQcaYHA7GUtXcMi6qK1IfssYaWbKs4Nbv9GmyodWPTCZEDFYzg=="],
|
||||
"@mintlify/cli": ["@mintlify/cli@4.0.1109", "", { "dependencies": { "@inquirer/prompts": "7.9.0", "@mintlify/common": "1.0.849", "@mintlify/link-rot": "3.0.1024", "@mintlify/prebuild": "1.0.991", "@mintlify/previewing": "4.0.1052", "@mintlify/validation": "0.1.663", "adm-zip": "0.5.16", "chalk": "5.2.0", "color": "4.2.3", "detect-port": "1.5.1", "front-matter": "4.0.2", "fs-extra": "11.2.0", "ink": "6.3.0", "inquirer": "12.3.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.2.0", "open": "^8.4.2", "openid-client": "^6.8.2", "posthog-node": "5.17.2", "react": "19.2.3", "semver": "7.7.2", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "^4.3.6" }, "optionalDependencies": { "keytar": "^7.9.0" }, "bin": { "mint": "bin/index.js", "mintlify": "bin/index.js" } }, "sha512-pw3XejzgnZLhMbcEUjMpS5+muIOWsxbpKVhikK3G3cmzq2FXtZgHpfH5JkA0uHPoNhacrPN2b3I3Aj2LyNKc/A=="],
|
||||
|
||||
"@mintlify/common": ["@mintlify/common@1.0.847", "", { "dependencies": { "@asyncapi/parser": "3.4.0", "@asyncapi/specs": "6.8.1", "@mintlify/mdx": "^3.0.4", "@mintlify/models": "0.0.290", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/validation": "0.1.662", "@sindresorhus/slugify": "2.2.0", "@types/mdast": "4.0.4", "acorn": "8.11.2", "acorn-jsx": "5.3.2", "color-blend": "4.0.0", "estree-util-to-js": "2.0.0", "estree-walker": "3.0.3", "front-matter": "4.0.2", "hast-util-from-html": "2.0.3", "hast-util-to-html": "9.0.4", "hast-util-to-text": "4.0.2", "hex-rgb": "5.0.0", "ignore": "7.0.5", "js-yaml": "4.1.0", "lodash": "4.17.21", "mdast-util-from-markdown": "2.0.2", "mdast-util-gfm": "3.0.0", "mdast-util-mdx": "3.0.0", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-gfm": "3.0.0", "micromark-extension-mdx-jsx": "3.0.1", "micromark-extension-mdxjs": "3.0.0", "openapi-types": "12.1.3", "postcss": "8.5.6", "rehype-stringify": "10.0.1", "remark": "15.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.0", "remark-math": "6.0.0", "remark-mdx": "3.1.0", "remark-parse": "11.0.0", "remark-rehype": "11.1.1", "remark-stringify": "11.0.0", "sucrase": "^3.34.0", "tailwindcss": "^3.4.17", "unified": "11.0.5", "unist-builder": "4.0.0", "unist-util-map": "4.0.0", "unist-util-remove": "4.0.0", "unist-util-remove-position": "5.0.0", "unist-util-visit": "5.0.0", "unist-util-visit-parents": "6.0.1", "vfile": "6.0.3", "xss": "1.0.15" } }, "sha512-6MNx0xn5M4KLSyWwbyUc3jpHUZ9vJFjRmRzo0n06Dt5OcpUFt89NwXYLQBbjNxFinklw9JN2+02vudoZsAdtnA=="],
|
||||
"@mintlify/common": ["@mintlify/common@1.0.849", "", { "dependencies": { "@asyncapi/parser": "3.4.0", "@asyncapi/specs": "6.8.1", "@mintlify/mdx": "^3.0.4", "@mintlify/models": "0.0.290", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/validation": "0.1.663", "@sindresorhus/slugify": "2.2.0", "@types/mdast": "4.0.4", "acorn": "8.11.2", "acorn-jsx": "5.3.2", "color-blend": "4.0.0", "estree-util-to-js": "2.0.0", "estree-walker": "3.0.3", "front-matter": "4.0.2", "hast-util-from-html": "2.0.3", "hast-util-to-html": "9.0.4", "hast-util-to-text": "4.0.2", "hex-rgb": "5.0.0", "ignore": "7.0.5", "js-yaml": "4.1.0", "lodash": "4.18.1", "mdast-util-from-markdown": "2.0.2", "mdast-util-gfm": "3.0.0", "mdast-util-mdx": "3.0.0", "mdast-util-mdx-jsx": "3.1.3", "micromark-extension-gfm": "3.0.0", "micromark-extension-mdx-jsx": "3.0.1", "micromark-extension-mdxjs": "3.0.0", "openapi-types": "12.1.3", "postcss": "8.5.6", "rehype-stringify": "10.0.1", "remark": "15.0.1", "remark-frontmatter": "5.0.0", "remark-gfm": "4.0.0", "remark-math": "6.0.0", "remark-mdx": "3.1.0", "remark-parse": "11.0.0", "remark-rehype": "11.1.1", "remark-stringify": "11.0.0", "sucrase": "^3.34.0", "tailwindcss": "^3.4.17", "unified": "11.0.5", "unist-builder": "4.0.0", "unist-util-map": "4.0.0", "unist-util-remove": "4.0.0", "unist-util-remove-position": "5.0.0", "unist-util-visit": "5.0.0", "unist-util-visit-parents": "6.0.1", "vfile": "6.0.3", "xss": "1.0.15" } }, "sha512-VGo1tRAImwccC/oyTSxD2rATKj2AQ9HR9mO5pbs4lJQGOTIfpAVtyie7iSohmtnu4WRJOBLRofSv/jgYGnG0zg=="],
|
||||
|
||||
"@mintlify/link-rot": ["@mintlify/link-rot@3.0.1022", "", { "dependencies": { "@mintlify/common": "1.0.847", "@mintlify/prebuild": "1.0.989", "@mintlify/previewing": "4.0.1050", "@mintlify/scraping": "4.0.522", "@mintlify/validation": "0.1.662", "fs-extra": "11.1.0", "unist-util-visit": "4.1.2" } }, "sha512-/I0FsmLToYiw6D8XNjWzQaexjsrYYosY0bPcQDO0xvfIYfYRX0jAyEoLJ3n6tGfHZYi5C666kT7ti5evIO37zg=="],
|
||||
"@mintlify/link-rot": ["@mintlify/link-rot@3.0.1024", "", { "dependencies": { "@mintlify/common": "1.0.849", "@mintlify/prebuild": "1.0.991", "@mintlify/previewing": "4.0.1052", "@mintlify/scraping": "4.0.522", "@mintlify/validation": "0.1.663", "fs-extra": "11.1.0", "unist-util-visit": "4.1.2" } }, "sha512-+0AeKU24PvdAguipUUmmGq17CzOq7X2B3FLUNig63Zc4v47t5DeSQ/WSOo36sNv6LD3c1//RJKBRO2gfa9XTnw=="],
|
||||
|
||||
"@mintlify/mdx": ["@mintlify/mdx@3.0.4", "", { "dependencies": { "@shikijs/transformers": "^3.11.0", "@shikijs/twoslash": "^3.12.2", "arktype": "^2.1.26", "hast-util-to-string": "^3.0.1", "mdast-util-from-markdown": "^2.0.2", "mdast-util-gfm": "^3.1.0", "mdast-util-mdx-jsx": "^3.2.0", "mdast-util-to-hast": "^13.2.0", "next-mdx-remote-client": "^1.0.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.0", "remark-math": "^6.0.0", "remark-smartypants": "^3.0.2", "shiki": "^3.11.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@radix-ui/react-popover": "^1.1.15", "react": "^18.3.1", "react-dom": "^18.3.1" } }, "sha512-tJhdpnM5ReJLNJ2fuDRIEr0zgVd6id7/oAIfs26V46QlygiLsc8qx4Rz3LWIX51rUXW/cfakjj0EATxIciIw+g=="],
|
||||
|
||||
@@ -1190,13 +1205,13 @@
|
||||
|
||||
"@mintlify/openapi-parser": ["@mintlify/openapi-parser@0.0.8", "", { "dependencies": { "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.4.5" } }, "sha512-9MBRq9lS4l4HITYCrqCL7T61MOb20q9IdU7HWhqYMNMM1jGO1nHjXasFy61yZ8V6gMZyyKQARGVoZ0ZrYN48Og=="],
|
||||
|
||||
"@mintlify/prebuild": ["@mintlify/prebuild@1.0.989", "", { "dependencies": { "@mintlify/common": "1.0.847", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/scraping": "4.0.711", "@mintlify/validation": "0.1.662", "chalk": "5.3.0", "favicons": "7.2.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "js-yaml": "4.1.0", "openapi-types": "12.1.3", "sharp": "0.33.5", "sharp-ico": "0.1.5", "unist-util-visit": "4.1.2", "uuid": "11.1.0" } }, "sha512-4ywkAhn2+F0vDEO3MtVmrl89gGx4uG3+2cqHDZgTfI88/VLSyryzT1GJKEI8xDSZtTCJpM/2A18fvhw7kPPxsA=="],
|
||||
"@mintlify/prebuild": ["@mintlify/prebuild@1.0.991", "", { "dependencies": { "@mintlify/common": "1.0.849", "@mintlify/openapi-parser": "^0.0.8", "@mintlify/scraping": "4.0.713", "@mintlify/validation": "0.1.663", "chalk": "5.3.0", "favicons": "7.2.0", "front-matter": "4.0.2", "fs-extra": "11.1.0", "js-yaml": "4.1.0", "openapi-types": "12.1.3", "sharp": "0.33.5", "sharp-ico": "0.1.5", "unist-util-visit": "4.1.2", "uuid": "11.1.0" } }, "sha512-pyQAWFYVmPnc20jpo6DnItmvHhJytKzqW/Q3GIYDqHsj/6JctcCfv07+Sc2m2bfu84kAd71yl1BIvXyBNFrD1Q=="],
|
||||
|
||||
"@mintlify/previewing": ["@mintlify/previewing@4.0.1050", "", { "dependencies": { "@mintlify/common": "1.0.847", "@mintlify/prebuild": "1.0.989", "@mintlify/validation": "0.1.662", "adm-zip": "0.5.16", "better-opn": "3.0.2", "chalk": "5.2.0", "chokidar": "3.5.3", "express": "4.18.2", "front-matter": "4.0.2", "fs-extra": "11.1.0", "got": "13.0.0", "ink": "6.3.0", "ink-spinner": "5.0.0", "is-online": "10.0.0", "js-yaml": "4.1.0", "openapi-types": "12.1.3", "react": "19.2.3", "socket.io": "4.7.2", "tar": "6.1.15", "unist-util-visit": "4.1.2", "yargs": "17.7.1" } }, "sha512-0NHlSkQRIrYKyHBsG6YFcilIVy1/1iEVkpdOlZJ3lSP8Mo40iiYlzT475GmBNuNBWMSd0CcH2PCJUWptWNDsXQ=="],
|
||||
"@mintlify/previewing": ["@mintlify/previewing@4.0.1052", "", { "dependencies": { "@mintlify/common": "1.0.849", "@mintlify/prebuild": "1.0.991", "@mintlify/validation": "0.1.663", "adm-zip": "0.5.16", "better-opn": "3.0.2", "chalk": "5.2.0", "chokidar": "3.5.3", "express": "4.18.2", "front-matter": "4.0.2", "fs-extra": "11.1.0", "got": "13.0.0", "ink": "6.3.0", "ink-spinner": "5.0.0", "is-online": "10.0.0", "js-yaml": "4.1.0", "openapi-types": "12.1.3", "react": "19.2.3", "socket.io": "4.7.2", "tar": "6.1.15", "unist-util-visit": "4.1.2", "yargs": "17.7.1" } }, "sha512-3d3PkyAk+7ErNQN7zJfIRKjlkpU4iHNVTFQLd38hi5717y6FH6+RpyoVT1iBsM0q6ZqW5ZZeDvdvVTF9obqnCQ=="],
|
||||
|
||||
"@mintlify/scraping": ["@mintlify/scraping@4.0.522", "", { "dependencies": { "@mintlify/common": "1.0.661", "@mintlify/openapi-parser": "^0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "22.14.0", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.21.4" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-PL2k52WT5S5OAgnT2K13bP7J2El6XwiVvQlrLvxDYw5KMMV+y34YVJI8ZscKb4trjitWDgyK0UTq2KN6NQgn6g=="],
|
||||
|
||||
"@mintlify/validation": ["@mintlify/validation@0.1.662", "", { "dependencies": { "@mintlify/mdx": "^3.0.4", "@mintlify/models": "0.0.290", "arktype": "2.1.27", "js-yaml": "4.1.0", "lcm": "0.0.3", "lodash": "4.17.21", "neotraverse": "0.6.18", "object-hash": "3.0.0", "openapi-types": "12.1.3", "uuid": "11.1.0", "zod": "3.24.0", "zod-to-json-schema": "3.20.4" } }, "sha512-yCKiIDDAZjJ6AQVg8bpJ+jU/8a7pSmqjmJfAjNBuvDoNYi1XhRSnSGDtDTfl+Ubx1mbwF5ryEc4nCEQZ0VShwQ=="],
|
||||
"@mintlify/validation": ["@mintlify/validation@0.1.663", "", { "dependencies": { "@mintlify/mdx": "^3.0.4", "@mintlify/models": "0.0.290", "arktype": "2.1.27", "js-yaml": "4.1.0", "lcm": "0.0.3", "lodash": "4.18.1", "neotraverse": "0.6.18", "object-hash": "3.0.0", "openapi-types": "12.1.3", "uuid": "11.1.0", "zod": "3.24.0", "zod-to-json-schema": "3.20.4" } }, "sha512-MU2PECIyOJaS0VHokMp4snEERSBzeHdMtGAMteRr9gl76hTlRKmp+i/9GUQL1XxrGfzY9s2D30YJjsMUUq2Lyw=="],
|
||||
|
||||
"@mishieck/ink-titled-box": ["@mishieck/ink-titled-box@0.3.0", "", { "peerDependencies": { "ink": "^6.0.0", "react": "^19.1.0", "typescript": "^5" } }, "sha512-ugzVH9hixp3hwKfQ8On/qnsrdAxS3y9rTu/aGOFed4zVUvtZyGZNIR4rxAwXult8HKI4vJEh0OM8wib9NPrwUg=="],
|
||||
|
||||
@@ -1426,7 +1441,7 @@
|
||||
|
||||
"@playwright/test": ["@playwright/test@1.58.1", "", { "dependencies": { "playwright": "1.58.1" }, "bin": { "playwright": "cli.js" } }, "sha512-6LdVIUERWxQMmUSSQi0I53GgCBYgM2RpGngCPY7hSeju+VrKjq3lvs7HpJoPbDiY5QM5EYRtRX5fvrinnMAz3w=="],
|
||||
|
||||
"@posthog/ai": ["@posthog/ai@7.13.2", "", { "dependencies": { "@anthropic-ai/sdk": "^0.78.0", "@google/genai": "^1.43.0", "@langchain/core": "^1.1.29", "@posthog/core": "1.25.2", "langchain": "^1.2.28", "openai": "^6.25.0", "uuid": "^11.1.0", "zod": "^4.1.13" }, "peerDependencies": { "@ai-sdk/provider": "^2.0.0 || ^3.0.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.200.0", "posthog-node": "^5.0.0" }, "optionalPeers": ["@ai-sdk/provider", "@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-http"] }, "sha512-1hnHYR7vvdIhg7gRcy77xFSuxRub8/xo9TsZze8q6u4XlHw+dJxyFTj0wKvzTxXZzZRmTXM0uLhgEWeZHuXmeA=="],
|
||||
"@posthog/ai": ["@posthog/ai@7.14.0", "", { "dependencies": { "@anthropic-ai/sdk": "^0.78.0", "@google/genai": "^1.43.0", "@langchain/core": "^1.1.29", "@posthog/core": "1.25.2", "langchain": "^1.2.28", "openai": "^6.25.0", "uuid": "^11.1.0", "zod": "^4.1.13" }, "peerDependencies": { "@ai-sdk/provider": "^2.0.0 || ^3.0.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/exporter-trace-otlp-http": "^0.200.0", "posthog-node": "^5.0.0" }, "optionalPeers": ["@ai-sdk/provider", "@opentelemetry/api", "@opentelemetry/exporter-trace-otlp-http"] }, "sha512-gKWim9vwPEtFMg2zrzPyf3UVb7YeLcsWFA8sRWyKXvxh04O92bbpG+v3OR3g6zVmE8wCrmXdtEs14IDgnmCz5g=="],
|
||||
|
||||
"@posthog/core": ["@posthog/core@1.25.2", "", {}, "sha512-h2FO7ut/BbfwpAXWpwdDHTzQgUo9ibDFEs6ZO+3cI3KPWQt5XwczK1OLAuPprcjm8T/jl0SH8jSFo5XdU4RbTg=="],
|
||||
|
||||
@@ -1922,6 +1937,8 @@
|
||||
|
||||
"@supabase/storage-js": ["@supabase/storage-js@2.103.0", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-DHmlvdAXwtOmZNbkIZi4lkobPR3XjIzoOgzoz5duMf6G+sDeY015YrzMJCnqdccuYr7X5x4yYuSwF//RoN2dvQ=="],
|
||||
|
||||
"@supabase/stripe-sync-engine": ["@supabase/stripe-sync-engine@0.48.5", "", { "dependencies": { "pg": "^8.20.0", "pg-node-migrations": "0.0.8", "yesql": "^7.0.0" }, "peerDependencies": { "stripe": "> 18" } }, "sha512-+LbtJH8n5Xiu289AL3FuWFdKXd0K7kDF0z4Lm+zMYoImWmOuGd3TgSx9gm/nv4nzLooOmIxGZh6LojoYBcJM+g=="],
|
||||
|
||||
"@supabase/supabase-js": ["@supabase/supabase-js@2.103.0", "", { "dependencies": { "@supabase/auth-js": "2.103.0", "@supabase/functions-js": "2.103.0", "@supabase/postgrest-js": "2.103.0", "@supabase/realtime-js": "2.103.0", "@supabase/storage-js": "2.103.0" } }, "sha512-j/6q5+LtXbR/YOLSLhy7Na74RD1cV2v+KwIIuuqMEjk1JpLEEyu0ynwDHpGoxMncDQl+R5FogaVqZm+85lZvtw=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
|
||||
@@ -1962,13 +1979,13 @@
|
||||
|
||||
"@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.4.3", "", { "bin": { "intent": "bin/intent.js" } }, "sha512-OZI6QyULw0FI0wjgmeYzCIfbgPsOEzwJtCpa69XrfLMtNXLGnz3d/dIabk7frg0TmHo+Ah49w5I4KC7Tufwsvw=="],
|
||||
|
||||
"@tanstack/form-core": ["@tanstack/form-core@1.28.6", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.9.1" } }, "sha512-4zroxL6VDj5O+w7l3dYZnUeL/h30KtNSV7UWzKAL7cl+8clMFdISPDlDlluS37As7oqvPVKo8B83VlIBvgmRog=="],
|
||||
"@tanstack/form-core": ["@tanstack/form-core@1.29.0", "", { "dependencies": { "@tanstack/devtools-event-client": "^0.4.1", "@tanstack/pacer-lite": "^0.1.1", "@tanstack/store": "^0.9.1" } }, "sha512-uyeKEdJBfbj0bkBSwvSYVRtWLOaXvfNX3CeVw1HqGOXVLxpBBGAqWdYLc+UoX/9xcoFwFXrjR9QqMPzvwm2yyQ=="],
|
||||
|
||||
"@tanstack/pacer-lite": ["@tanstack/pacer-lite@0.1.1", "", {}, "sha512-y/xtNPNt/YeyoVxE/JCx+T7yjEzpezmbb+toK8DDD1P4m7Kzs5YR956+7OKexG3f8aXgC3rLZl7b1V+yNUSy5w=="],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.85.6", "", {}, "sha512-hCj0TktzdCv2bCepIdfwqVwUVWb+GSHm1Jnn8w+40lfhQ3m7lCO7ADRUJy+2unxQ/nzjh2ipC6ye69NDW3l73g=="],
|
||||
|
||||
"@tanstack/react-form": ["@tanstack/react-form@1.28.6", "", { "dependencies": { "@tanstack/form-core": "1.28.6", "@tanstack/react-store": "^0.9.1" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dRxwKeNW3uuJvf0sXsIQ2compFMnIJNk9B436Lx0fqkqK+CBvA1tNmEdX+faoCpuQ5Wua3c8ahVibJ65cpkijA=="],
|
||||
"@tanstack/react-form": ["@tanstack/react-form@1.29.0", "", { "dependencies": { "@tanstack/form-core": "1.29.0", "@tanstack/react-store": "^0.9.1" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-jj425NNX0QKqbUzqSNiYI3HCPHSk2df47acXCJyXczWOTmG81ECZGkgofgqamFsSU9kMiH6Di5RLUnftrlhWSw=="],
|
||||
|
||||
"@tanstack/react-query": ["@tanstack/react-query@5.85.6", "", { "dependencies": { "@tanstack/query-core": "5.85.6" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-VUAag4ERjh+qlmg0wNivQIVCZUrYndqYu3/wPCVZd4r0E+1IqotbeyGTc+ICroL/PqbpSaGZg02zSWYfcvxbdA=="],
|
||||
|
||||
@@ -3798,7 +3815,7 @@
|
||||
|
||||
"ksuid": ["ksuid@3.0.0", "", { "dependencies": { "base-convert-int-array": "^1.0.1" } }, "sha512-81CkBGn/06ZVAjGvFZi6fVG8VcPeMH0JpJ4V1Z9VwrMMaGIeAjY4jrVdrIcxhL9I2ZUU6t5uiyswcmkk+KZegA=="],
|
||||
|
||||
"kysely": ["kysely@0.28.15", "", {}, "sha512-r2clcf7HLWvDXaVUEvQymXJY4i3bSOIV3xsL/Upy3ZfSv5HeKsk9tsqbBptLvth5qHEIhxeHTA2jNLyQABkLBA=="],
|
||||
"kysely": ["kysely@0.28.16", "", {}, "sha512-3i5pmOiZvMDj00qhrIVbH0AnioVTx22DMP7Vn5At4yJO46iy+FM8Y/g61ltenLVSo3fiO8h8Q3QOFgf/gQ72ww=="],
|
||||
|
||||
"langchain": ["langchain@1.3.1", "", { "dependencies": { "@langchain/langgraph": "^1.2.8", "@langchain/langgraph-checkpoint": "^1.0.1", "langsmith": ">=0.5.0 <1.0.0", "uuid": "^11.1.0", "zod": "^3.25.76 || ^4" }, "peerDependencies": { "@langchain/core": "^1.1.39" } }, "sha512-tJu8Ibf3NAuDW8pMT7VIBCc96m8TymjSMqRBpQawlG8zeTGPZK5TKA5gBYRNZvrz0YkJA57O2rcjP+mIxrS0+g=="],
|
||||
|
||||
@@ -4102,7 +4119,7 @@
|
||||
|
||||
"minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
|
||||
|
||||
"mintlify": ["mintlify@4.2.504", "", { "dependencies": { "@mintlify/cli": "4.0.1107" }, "bin": { "mintlify": "index.js" } }, "sha512-N7Upy3DHiuvVzith7MTI8cRRGuzlYgAAKjMgIeWi4Cz8JQVDH94NbJ+icl3gVhef6DRtZkmYHzpeZRpYATASpA=="],
|
||||
"mintlify": ["mintlify@4.2.506", "", { "dependencies": { "@mintlify/cli": "4.0.1109" }, "bin": { "mintlify": "index.js" } }, "sha512-RpFXl57t0tX5NuPbw+69O7kKW2AFTHf5UdwPWHAd9NKXyPjj4K7FxXB1DlkEHyvp6Yhewr6LTjJigNFp4vrPQw=="],
|
||||
|
||||
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||
|
||||
@@ -4368,6 +4385,8 @@
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
"pg-node-migrations": ["pg-node-migrations@0.0.8", "", { "dependencies": { "pg": "^8.6.0", "sql-template-strings": "^2.2.2" }, "bin": { "pg-validate-migrations": "dist/bin/validate.js" } }, "sha512-44cMl9umOmCv0hzZyEcvjEq8Bm8u7mrzggZ06qXTJVSsMMB4j2OsjG+rSp+uzeKWyP2Vu0K9Ye2wKtjFUJwrdw=="],
|
||||
|
||||
"pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="],
|
||||
|
||||
"pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="],
|
||||
@@ -4864,6 +4883,8 @@
|
||||
|
||||
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
|
||||
|
||||
"sql-template-strings": ["sql-template-strings@2.2.2", "", {}, "sha512-UXhXR2869FQaD+GMly8jAMCRZ94nU5KcrFetZfWEMd+LVVG6y0ExgHAhatEcKZ/wk8YcKPdi+hiD2wm75lq3/Q=="],
|
||||
|
||||
"sqs-consumer": ["sqs-consumer@6.0.2", "", { "dependencies": { "@aws-sdk/client-sqs": "^3.226.0", "debug": "^4.3.4" } }, "sha512-Y8ztFBc1VPj4q72j9Uji4XR6n2O88n9X83aOUaCvz9zZqjpGJaI4PBHBPnE8RDBgD0EQR8Dh+sQwH/+8mzr+Bg=="],
|
||||
|
||||
"stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="],
|
||||
@@ -5374,6 +5395,8 @@
|
||||
|
||||
"yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
|
||||
|
||||
"yesql": ["yesql@7.0.0", "", {}, "sha512-sosfr7agy4ibLM7BvXBkM6BpBmKMGuBO8DUYQEuey+QqaqrgW+2bsSg6D050ocBYIz0PuHxUyehyzEztZTU4pw=="],
|
||||
|
||||
"yn": ["yn@3.1.1", "", {}, "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
||||
@@ -5754,7 +5777,7 @@
|
||||
|
||||
"@mintlify/openapi-parser/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"@mintlify/prebuild/@mintlify/scraping": ["@mintlify/scraping@4.0.711", "", { "dependencies": { "@mintlify/common": "1.0.847", "@mintlify/openapi-parser": "^0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "22.14.0", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.24.0" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-e0tWdbZoxZ+/OKm29stpAoJkevL1hawpCdsTKhV5/9O8o4cTpLhEnAkS6fSlBv5t0k/Ut/FQeW10kIfgXj+Yww=="],
|
||||
"@mintlify/prebuild/@mintlify/scraping": ["@mintlify/scraping@4.0.713", "", { "dependencies": { "@mintlify/common": "1.0.849", "@mintlify/openapi-parser": "^0.0.8", "fs-extra": "11.1.1", "hast-util-to-mdast": "10.1.0", "js-yaml": "4.1.0", "mdast-util-mdx-jsx": "3.1.3", "neotraverse": "0.6.18", "puppeteer": "22.14.0", "rehype-parse": "9.0.1", "remark-gfm": "4.0.0", "remark-mdx": "3.0.1", "remark-parse": "11.0.0", "remark-stringify": "11.0.0", "unified": "11.0.5", "unist-util-visit": "5.0.0", "yargs": "17.7.1", "zod": "3.24.0" }, "bin": { "mintlify-scrape": "bin/cli.js" } }, "sha512-V0+gUqL3LgJygTL7PunRMaxirHp0oGKCLyFNmAQMJ3gX4WPUBYc+5GLiJ7hQn6lsp6m6wLmztVpeKdX8OwD8GA=="],
|
||||
|
||||
"@mintlify/prebuild/chalk": ["chalk@5.3.0", "", {}, "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w=="],
|
||||
|
||||
@@ -5928,20 +5951,30 @@
|
||||
|
||||
"@opentelemetry/exporter-zipkin/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="],
|
||||
|
||||
"@opentelemetry/instrumentation-amqplib/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"@opentelemetry/instrumentation-amqplib/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-connect/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"@opentelemetry/instrumentation-connect/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-dataloader/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-express/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"@opentelemetry/instrumentation-express/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-fs/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"@opentelemetry/instrumentation-fs/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-generic-pool/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-graphql/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-hapi/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"@opentelemetry/instrumentation-hapi/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-http/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="],
|
||||
@@ -5954,18 +5987,24 @@
|
||||
|
||||
"@opentelemetry/instrumentation-knex/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-koa/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"@opentelemetry/instrumentation-koa/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-lru-memoizer/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-mongodb/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-mongoose/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"@opentelemetry/instrumentation-mongoose/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-mysql/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-mysql2/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-pg/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"@opentelemetry/instrumentation-pg/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-pg/@types/pg": ["@types/pg@8.15.6", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ=="],
|
||||
@@ -5974,6 +6013,8 @@
|
||||
|
||||
"@opentelemetry/instrumentation-tedious/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/instrumentation-undici/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"@opentelemetry/instrumentation-undici/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@opentelemetry/otlp-grpc-exporter-base/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="],
|
||||
@@ -6014,6 +6055,8 @@
|
||||
|
||||
"@opentelemetry/sdk-trace-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.0.1", "", { "dependencies": { "@opentelemetry/core": "2.0.1", "@opentelemetry/resources": "2.0.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ=="],
|
||||
|
||||
"@opentelemetry/sql-common/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"@orpc/openapi/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="],
|
||||
|
||||
"@orpc/server/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
@@ -6078,8 +6121,14 @@
|
||||
|
||||
"@sentry/node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/instrumentation": ["@opentelemetry/instrumentation@0.211.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.211.0", "import-in-the-middle": "^2.0.0", "require-in-the-middle": "^8.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"@sentry/node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
|
||||
|
||||
"@sentry/node/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="],
|
||||
|
||||
"@sentry/node-core/import-in-the-middle": ["import-in-the-middle@2.0.6", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw=="],
|
||||
@@ -6662,6 +6711,8 @@
|
||||
|
||||
"posthog-js/@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/otlp-exporter-base": "0.208.0", "@opentelemetry/otlp-transformer": "0.208.0", "@opentelemetry/sdk-logs": "0.208.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg=="],
|
||||
|
||||
"posthog-js/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
|
||||
"posthog-js/@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA=="],
|
||||
|
||||
"prebuild-install/tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="],
|
||||
@@ -8138,6 +8189,8 @@
|
||||
|
||||
"posthog-js/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.208.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.208.0", "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", "@opentelemetry/sdk-logs": "0.208.0", "@opentelemetry/sdk-metrics": "2.2.0", "@opentelemetry/sdk-trace-base": "2.2.0", "protobufjs": "^7.3.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ=="],
|
||||
|
||||
"posthog-js/@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
|
||||
"posthog-js/@opentelemetry/sdk-logs/@opentelemetry/core": ["@opentelemetry/core@2.2.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw=="],
|
||||
|
||||
"posthog-js/@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="],
|
||||
|
||||
@@ -16,6 +16,7 @@ COPY packages/atmn/package.json ./packages/atmn/package.json
|
||||
COPY packages/atmn-tests/package.json ./packages/atmn-tests/package.json
|
||||
COPY packages/openapi/package.json ./packages/openapi/package.json
|
||||
COPY packages/ksuid/package.json ./packages/ksuid/package.json
|
||||
COPY packages/stripe-sync/package.json ./packages/stripe-sync/package.json
|
||||
COPY packages/sdk/package.json ./packages/sdk/package.json
|
||||
COPY apps/sdk-test/package.json ./apps/sdk-test/package.json
|
||||
COPY apps/docs/package.json ./apps/docs/package.json
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
"packages/sdk",
|
||||
"packages/autumn-js",
|
||||
"packages/openapi",
|
||||
"packages/ksuid"
|
||||
"packages/ksuid",
|
||||
"packages/stripe-sync"
|
||||
],
|
||||
"catalog": {
|
||||
"stripe": "19.3.0-beta.1",
|
||||
@@ -108,7 +109,9 @@
|
||||
"js:publish": "bun js:build && cd packages/autumn-js && npm publish",
|
||||
"js:publish-beta": "bun js:build && cd packages/autumn-js && npm publish --tag beta",
|
||||
"js:publish-dry": "bun js:build && cd packages/autumn-js && npm publish --dry-run",
|
||||
"js:version": "git fetch --tags origin && git tag -l 'autumn-js-v*' --sort=-v:refname | head -1"
|
||||
"js:version": "git fetch --tags origin && git tag -l 'autumn-js-v*' --sort=-v:refname | head -1",
|
||||
"stripe-sync:migrate": "bun packages/stripe-sync/scripts/migrate.ts",
|
||||
"stripe-sync:migrate:prod": "NODE_ENV=production infisical run --env=prod -- bun packages/stripe-sync/scripts/migrate.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sqs": "^3.985.0",
|
||||
|
||||
24
packages/stripe-sync/package.json
Normal file
24
packages/stripe-sync/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@autumn/stripe-sync",
|
||||
"version": "1.0.0",
|
||||
"description": "Stripe-to-Postgres sync engine wrapper for Autumn",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./src/index.ts",
|
||||
"import": "./src/index.ts",
|
||||
"default": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@supabase/stripe-sync-engine": "^0.48.5",
|
||||
"pg": "^8.20.0",
|
||||
"stripe": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pg": "^8.20.0"
|
||||
}
|
||||
}
|
||||
26
packages/stripe-sync/scripts/migrate.ts
Normal file
26
packages/stripe-sync/scripts/migrate.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { config } from "dotenv";
|
||||
import { addAutumnColumns, runStripeSyncMigrations } from "../src/index.js";
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, "../../..");
|
||||
|
||||
config({ path: path.join(repoRoot, "server/.env"), override: true });
|
||||
|
||||
const databaseUrl = process.env.STRIPE_SYNC_DATABASE_URL;
|
||||
|
||||
if (!databaseUrl) {
|
||||
console.error("STRIPE_SYNC_DATABASE_URL is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Running stripe sync migrations...");
|
||||
await runStripeSyncMigrations({ databaseUrl });
|
||||
console.log("Stripe sync migrations complete");
|
||||
|
||||
console.log(
|
||||
"Adding Autumn columns (stripe_account_id, org_id) to synced tables...",
|
||||
);
|
||||
await addAutumnColumns({ databaseUrl });
|
||||
console.log("Autumn columns added");
|
||||
38
packages/stripe-sync/src/addAccountIdColumn.ts
Normal file
38
packages/stripe-sync/src/addAccountIdColumn.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import pg from "pg";
|
||||
import { SYNCED_TABLES } from "./eventTypeToTable.js";
|
||||
|
||||
/**
|
||||
* Adds `stripe_account_id` and `org_id` columns + indexes to all synced tables.
|
||||
* Idempotent -- safe to run multiple times.
|
||||
*/
|
||||
export const addAutumnColumns = async ({
|
||||
databaseUrl,
|
||||
schema = "stripe",
|
||||
}: {
|
||||
databaseUrl: string;
|
||||
schema?: string;
|
||||
}): Promise<void> => {
|
||||
const client = new pg.Client({ connectionString: databaseUrl });
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
for (const table of SYNCED_TABLES) {
|
||||
await client.query(`
|
||||
ALTER TABLE "${schema}"."${table}"
|
||||
ADD COLUMN IF NOT EXISTS stripe_account_id TEXT,
|
||||
ADD COLUMN IF NOT EXISTS org_id TEXT,
|
||||
ADD COLUMN IF NOT EXISTS env TEXT
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_${table}_stripe_account_id
|
||||
ON "${schema}"."${table}" (stripe_account_id)
|
||||
`);
|
||||
await client.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_${table}_org_id
|
||||
ON "${schema}"."${table}" (org_id)
|
||||
`);
|
||||
}
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
};
|
||||
61
packages/stripe-sync/src/eventTypeToTable.ts
Normal file
61
packages/stripe-sync/src/eventTypeToTable.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Maps a Stripe event type to the sync DB table that stores the object.
|
||||
* Returns undefined for event types that don't map to a known table.
|
||||
*/
|
||||
export const eventTypeToTable = ({
|
||||
eventType,
|
||||
}: {
|
||||
eventType: string;
|
||||
}): string | undefined => {
|
||||
if (eventType.startsWith("charge.dispute.")) return "disputes";
|
||||
if (eventType.startsWith("charge.")) return "charges";
|
||||
if (eventType.startsWith("checkout.session.")) return "checkout_sessions";
|
||||
if (eventType.startsWith("customer.subscription.")) return "subscriptions";
|
||||
if (eventType.startsWith("customer.tax_id.")) return "tax_ids";
|
||||
if (eventType.startsWith("customer.")) return "customers";
|
||||
if (eventType.startsWith("invoice.")) return "invoices";
|
||||
if (eventType.startsWith("product.")) return "products";
|
||||
if (eventType.startsWith("price.")) return "prices";
|
||||
if (eventType.startsWith("plan.")) return "plans";
|
||||
if (eventType.startsWith("setup_intent.")) return "setup_intents";
|
||||
if (eventType.startsWith("subscription_schedule."))
|
||||
return "subscription_schedules";
|
||||
if (eventType.startsWith("payment_method.")) return "payment_methods";
|
||||
if (eventType.startsWith("payment_intent.")) return "payment_intents";
|
||||
if (eventType.startsWith("credit_note.")) return "credit_notes";
|
||||
if (eventType.startsWith("radar.early_fraud_warning."))
|
||||
return "early_fraud_warnings";
|
||||
if (eventType.startsWith("refund.")) return "refunds";
|
||||
if (eventType.startsWith("review.")) return "reviews";
|
||||
if (eventType === "invoice_payment.paid") return "invoice_payments";
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** All tables in the stripe sync schema that store Stripe objects. */
|
||||
export const SYNCED_TABLES = [
|
||||
"charges",
|
||||
"checkout_sessions",
|
||||
"checkout_session_line_items",
|
||||
"coupons",
|
||||
"credit_notes",
|
||||
"customers",
|
||||
"disputes",
|
||||
"early_fraud_warnings",
|
||||
"events",
|
||||
"invoices",
|
||||
"invoice_payments",
|
||||
"payment_intents",
|
||||
"payment_methods",
|
||||
"payouts",
|
||||
"plans",
|
||||
"prices",
|
||||
"products",
|
||||
"refunds",
|
||||
"reviews",
|
||||
"setup_intents",
|
||||
"subscription_items",
|
||||
"subscription_schedules",
|
||||
"subscriptions",
|
||||
"tax_ids",
|
||||
] as const;
|
||||
12
packages/stripe-sync/src/index.ts
Normal file
12
packages/stripe-sync/src/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export { addAutumnColumns } from "./addAccountIdColumn.js";
|
||||
export { eventTypeToTable, SYNCED_TABLES } from "./eventTypeToTable.js";
|
||||
export {
|
||||
closeStripeSyncEngine,
|
||||
getStripeSyncEngine,
|
||||
processStripeSyncEvent,
|
||||
} from "./initStripeSync.js";
|
||||
export { runStripeSyncMigrations } from "./runStripeSyncMigrations.js";
|
||||
export {
|
||||
isSyncableEvent,
|
||||
SYNCABLE_EVENT_PREFIXES,
|
||||
} from "./syncableResources.js";
|
||||
102
packages/stripe-sync/src/initStripeSync.ts
Normal file
102
packages/stripe-sync/src/initStripeSync.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { StripeSync } from "@supabase/stripe-sync-engine";
|
||||
import type Stripe from "stripe";
|
||||
import { eventTypeToTable } from "./eventTypeToTable.js";
|
||||
|
||||
const SCHEMA = "stripe";
|
||||
|
||||
let instance: StripeSync | null = null;
|
||||
let initAttempted = false;
|
||||
|
||||
/**
|
||||
* Lazily creates a singleton StripeSync instance.
|
||||
* Returns null if STRIPE_SYNC_DATABASE_URL is not configured or init fails.
|
||||
*/
|
||||
export const getStripeSyncEngine = (): StripeSync | null => {
|
||||
if (instance) return instance;
|
||||
if (initAttempted) return null;
|
||||
|
||||
initAttempted = true;
|
||||
|
||||
const databaseUrl = process.env.STRIPE_SYNC_DATABASE_URL;
|
||||
|
||||
const stripeSecretKey =
|
||||
process.env.STRIPE_LIVE_SECRET_KEY || process.env.STRIPE_SANDBOX_SECRET_KEY;
|
||||
|
||||
if (!databaseUrl || !stripeSecretKey) return null;
|
||||
|
||||
try {
|
||||
instance = new StripeSync({
|
||||
poolConfig: {
|
||||
connectionString: databaseUrl,
|
||||
max: 5,
|
||||
keepAlive: true,
|
||||
connectionTimeoutMillis: 5_000,
|
||||
idleTimeoutMillis: 30_000,
|
||||
},
|
||||
stripeSecretKey,
|
||||
stripeWebhookSecret: "unused-processEvent-only",
|
||||
schema: SCHEMA,
|
||||
});
|
||||
} catch {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
/**
|
||||
* Upserts the Stripe event into the sync DB, then stamps the row
|
||||
* with the originating Stripe account ID and org ID for multi-tenancy.
|
||||
* Fully fail-open: any error is swallowed and returns silently.
|
||||
*/
|
||||
export const processStripeSyncEvent = async ({
|
||||
event,
|
||||
stripeAccountId,
|
||||
orgId,
|
||||
env,
|
||||
}: {
|
||||
event: Stripe.Event;
|
||||
stripeAccountId?: string;
|
||||
orgId?: string;
|
||||
env?: string;
|
||||
}): Promise<void> => {
|
||||
const engine = getStripeSyncEngine();
|
||||
if (!engine) return;
|
||||
|
||||
try {
|
||||
await engine.processEvent(event);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const table = eventTypeToTable({ eventType: event.type });
|
||||
if (!table) return;
|
||||
|
||||
const objectId = (event.data.object as { id?: string }).id;
|
||||
if (!objectId) return;
|
||||
|
||||
const accountId = stripeAccountId ?? event.account ?? null;
|
||||
|
||||
if (!accountId && !orgId && !env) return;
|
||||
|
||||
try {
|
||||
await engine.postgresClient.pool.query(
|
||||
`UPDATE "${SCHEMA}"."${table}" SET stripe_account_id = COALESCE($1, stripe_account_id), org_id = COALESCE($2, org_id), env = COALESCE($3, env) WHERE id = $4`,
|
||||
[accountId, orgId, env, objectId],
|
||||
);
|
||||
} catch {
|
||||
// Fail-open: metadata stamp is best-effort
|
||||
}
|
||||
};
|
||||
|
||||
/** Gracefully close the sync engine's PG pool (call on server shutdown). */
|
||||
export const closeStripeSyncEngine = async (): Promise<void> => {
|
||||
if (!instance) return;
|
||||
try {
|
||||
await instance.close();
|
||||
} catch {
|
||||
// Best-effort cleanup
|
||||
}
|
||||
instance = null;
|
||||
initAttempted = false;
|
||||
};
|
||||
15
packages/stripe-sync/src/runStripeSyncMigrations.ts
Normal file
15
packages/stripe-sync/src/runStripeSyncMigrations.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { runMigrations } from "@supabase/stripe-sync-engine";
|
||||
|
||||
/**
|
||||
* Run stripe-sync-engine migrations against the sync DB.
|
||||
* Idempotent -- safe to run multiple times.
|
||||
*/
|
||||
export const runStripeSyncMigrations = async ({
|
||||
databaseUrl,
|
||||
schema = "stripe",
|
||||
}: {
|
||||
databaseUrl: string;
|
||||
schema?: string;
|
||||
}): Promise<void> => {
|
||||
await runMigrations({ databaseUrl, schema });
|
||||
};
|
||||
16
packages/stripe-sync/src/syncableResources.ts
Normal file
16
packages/stripe-sync/src/syncableResources.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export const SYNCABLE_EVENT_PREFIXES = [
|
||||
"customer.subscription.",
|
||||
"payment_intent.",
|
||||
"invoice.",
|
||||
"customer.",
|
||||
"product.",
|
||||
"price.",
|
||||
] as const;
|
||||
|
||||
export const isSyncableEvent = ({
|
||||
eventType,
|
||||
}: {
|
||||
eventType: string;
|
||||
}): boolean => {
|
||||
return SYNCABLE_EVENT_PREFIXES.some((prefix) => eventType.startsWith(prefix));
|
||||
};
|
||||
16
packages/stripe-sync/tsconfig.json
Normal file
16
packages/stripe-sync/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "Preserve",
|
||||
"moduleResolution": "bundler",
|
||||
"target": "ES2020",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "."
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
@@ -24,17 +24,18 @@ const main = async () => {
|
||||
|
||||
const { db } = initDrizzle();
|
||||
|
||||
const query = getFullCusQuery(
|
||||
customerId,
|
||||
const query = getFullCusQuery({
|
||||
idOrInternalId: customerId,
|
||||
orgId,
|
||||
env,
|
||||
RELEVANT_STATUSES,
|
||||
true, // includeInvoices
|
||||
true, // withEntities
|
||||
false, // withTrialsUsed
|
||||
true, // withSubs
|
||||
false, // withEvents
|
||||
);
|
||||
inStatuses: RELEVANT_STATUSES,
|
||||
includeInvoices: true,
|
||||
withEntities: true,
|
||||
withTrialsUsed: false,
|
||||
withSubs: true,
|
||||
withEvents: false,
|
||||
cusProductLimit: 15,
|
||||
});
|
||||
|
||||
// Run the actual query to measure wall-clock time
|
||||
console.log("--- Running query ---");
|
||||
|
||||
@@ -108,6 +108,7 @@ const main = async () => {
|
||||
limit: config.limit,
|
||||
offset: config.offset,
|
||||
search: config.search,
|
||||
cusProductLimit: 15,
|
||||
});
|
||||
const filteredCountQuery = getFilteredCountQuery({
|
||||
orgId: config.orgId,
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@autumn/ksuid": "workspace:*",
|
||||
"@autumn/shared": "workspace:*",
|
||||
"@autumn/stripe-sync": "workspace:*",
|
||||
"@aws-sdk/client-s3": "^3.1017.0",
|
||||
"@aws-sdk/client-scheduler": "^3.1004.0",
|
||||
"@aws-sdk/client-sqs": "^3.958.0",
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
import "dotenv/config";
|
||||
import { loadLocalEnv } from "./src/utils/envUtils";
|
||||
import Stripe from "stripe";
|
||||
import {
|
||||
MAIN_STRIPE_EVENT_TYPES,
|
||||
SYNC_STRIPE_EVENT_TYPES,
|
||||
} from "./src/external/stripe/common/stripeConstants";
|
||||
|
||||
loadLocalEnv();
|
||||
|
||||
const allEventTypes = [
|
||||
...new Set([...MAIN_STRIPE_EVENT_TYPES, ...SYNC_STRIPE_EVENT_TYPES]),
|
||||
] as Stripe.WebhookEndpointCreateParams.EnabledEvent[];
|
||||
|
||||
const main = async () => {
|
||||
const stripe = new Stripe(process.env.STRIPE_SANDBOX_SECRET_KEY || "");
|
||||
|
||||
|
||||
const result = await stripe.webhookEndpoints.create({
|
||||
url: `${process.env.STRIPE_WEBHOOK_URL}/webhooks/connect/sandbox`,
|
||||
enabled_events: [
|
||||
"checkout.session.completed",
|
||||
"customer.subscription.created",
|
||||
"customer.subscription.updated",
|
||||
"customer.subscription.deleted",
|
||||
"customer.discount.deleted",
|
||||
"invoice.paid",
|
||||
"invoice.upcoming",
|
||||
"invoice.created",
|
||||
"invoice.finalized",
|
||||
"invoice.updated",
|
||||
"subscription_schedule.canceled",
|
||||
"subscription_schedule.updated",
|
||||
],
|
||||
enabled_events: allEventTypes,
|
||||
connect: true,
|
||||
});
|
||||
|
||||
|
||||
1
server/src/external/aws/s3/adminS3Config.ts
vendored
1
server/src/external/aws/s3/adminS3Config.ts
vendored
@@ -2,6 +2,7 @@ export const ADMIN_REQUEST_BLOCK_CONFIG_KEY = "admin/request-block-config.json";
|
||||
export const ADMIN_FEATURE_FLAGS_CONFIG_KEY = "admin/feature-flags-config.json";
|
||||
export const ADMIN_CUSTOMER_BLOCK_CONFIG_KEY =
|
||||
"admin/customer-block-config.json";
|
||||
export const ADMIN_ORG_LIMITS_CONFIG_KEY = "admin/org-limits-config.json";
|
||||
|
||||
type AdminS3Target = "dev" | "prod";
|
||||
|
||||
|
||||
64
server/src/external/stripe/common/stripeConstants.ts
vendored
Normal file
64
server/src/external/stripe/common/stripeConstants.ts
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
import type Stripe from "stripe";
|
||||
|
||||
type StripeEventType = Stripe.WebhookEndpointCreateParams.EnabledEvent;
|
||||
|
||||
/** Events Autumn actively handles in its webhook handler. */
|
||||
export const MAIN_STRIPE_EVENT_TYPES: StripeEventType[] = [
|
||||
"checkout.session.completed",
|
||||
"customer.subscription.created",
|
||||
"customer.subscription.updated",
|
||||
"customer.subscription.deleted",
|
||||
"customer.discount.deleted",
|
||||
"invoice.paid",
|
||||
"invoice.upcoming",
|
||||
"invoice.created",
|
||||
"invoice.finalized",
|
||||
"invoice.updated",
|
||||
"subscription_schedule.canceled",
|
||||
"subscription_schedule.updated",
|
||||
];
|
||||
|
||||
/** Additional events needed to keep the stripe-sync DB up to date. */
|
||||
export const SYNC_STRIPE_EVENT_TYPES: StripeEventType[] = [
|
||||
// customers
|
||||
"customer.created",
|
||||
"customer.updated",
|
||||
"customer.deleted",
|
||||
|
||||
// subscriptions (extras beyond main)
|
||||
"customer.subscription.paused",
|
||||
"customer.subscription.resumed",
|
||||
|
||||
// subscription schedules (extras beyond main)
|
||||
"subscription_schedule.created",
|
||||
"subscription_schedule.completed",
|
||||
"subscription_schedule.released",
|
||||
|
||||
// payment methods
|
||||
"payment_method.attached",
|
||||
"payment_method.detached",
|
||||
"payment_method.updated",
|
||||
|
||||
// products
|
||||
"product.created",
|
||||
"product.updated",
|
||||
"product.deleted",
|
||||
|
||||
// prices
|
||||
"price.created",
|
||||
"price.updated",
|
||||
"price.deleted",
|
||||
|
||||
// invoices (extras beyond main)
|
||||
"invoice.deleted",
|
||||
"invoice.payment_failed",
|
||||
"invoice.payment_succeeded",
|
||||
"invoice.voided",
|
||||
"invoice.marked_uncollectible",
|
||||
|
||||
// payment intents
|
||||
"payment_intent.created",
|
||||
"payment_intent.succeeded",
|
||||
"payment_intent.payment_failed",
|
||||
"payment_intent.canceled",
|
||||
];
|
||||
@@ -1,6 +1,10 @@
|
||||
import { type AppEnv, ErrCode } from "@autumn/shared";
|
||||
import Stripe from "stripe";
|
||||
import RecaseError from "@/utils/errorUtils.js";
|
||||
import {
|
||||
MAIN_STRIPE_EVENT_TYPES,
|
||||
SYNC_STRIPE_EVENT_TYPES,
|
||||
} from "./common/stripeConstants";
|
||||
|
||||
export const checkKeyValid = async (apiKey: string) => {
|
||||
const stripe = new Stripe(apiKey);
|
||||
@@ -29,20 +33,22 @@ export const createWebhookEndpoint = async (
|
||||
|
||||
const endpoint = await stripe.webhookEndpoints.create({
|
||||
url: `${webhookBaseUrl}/webhooks/stripe/${orgId}/${env}`,
|
||||
enabled_events: [
|
||||
"customer.subscription.created",
|
||||
"customer.subscription.updated",
|
||||
"customer.subscription.deleted",
|
||||
"checkout.session.completed",
|
||||
"invoice.paid",
|
||||
"invoice.upcoming",
|
||||
"invoice.created",
|
||||
"invoice.finalized",
|
||||
"invoice.updated",
|
||||
"subscription_schedule.canceled",
|
||||
"subscription_schedule.updated",
|
||||
"customer.discount.deleted",
|
||||
],
|
||||
enabled_events: [...MAIN_STRIPE_EVENT_TYPES, ...SYNC_STRIPE_EVENT_TYPES],
|
||||
|
||||
// [
|
||||
// "customer.subscription.created",
|
||||
// "customer.subscription.updated",
|
||||
// "customer.subscription.deleted",
|
||||
// "checkout.session.completed",
|
||||
// "invoice.paid",
|
||||
// "invoice.upcoming",
|
||||
// "invoice.created",
|
||||
// "invoice.finalized",
|
||||
// "invoice.updated",
|
||||
// "subscription_schedule.canceled",
|
||||
// "subscription_schedule.updated",
|
||||
// "customer.discount.deleted",
|
||||
// ],
|
||||
});
|
||||
|
||||
return endpoint;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { handleStripeWebhookEvent } from "./handleStripeWebhookEvent.js";
|
||||
import { stripeConnectSeederMiddleware } from "./webhookMiddlewares/stripeConnectSeederMiddleware.js";
|
||||
import { stripeIdempotencyMiddleware } from "./webhookMiddlewares/stripeIdempotencyMiddleware.js";
|
||||
import { stripeLegacySeederMiddleware } from "./webhookMiddlewares/stripeLegacySeederMiddleware.js";
|
||||
import { stripeSyncMiddleware } from "./webhookMiddlewares/stripeSyncMiddleware.js";
|
||||
import { stripeToAutumnCustomerMiddleware } from "./webhookMiddlewares/stripeToAutumnCustomerMiddleware.js";
|
||||
import type { StripeWebhookHonoEnv } from "./webhookMiddlewares/stripeWebhookContext.js";
|
||||
import { stripeWebhookRefreshMiddleware } from "./webhookMiddlewares/stripeWebhookRefreshMiddleware.js";
|
||||
@@ -15,6 +16,7 @@ stripeWebhookRouter.post(
|
||||
"/webhooks/stripe/:orgId/:env",
|
||||
stripeLegacySeederMiddleware,
|
||||
stripeWebhookRefreshMiddleware,
|
||||
stripeSyncMiddleware,
|
||||
stripeToAutumnCustomerMiddleware,
|
||||
stripeLoggerMiddleware,
|
||||
stripeIdempotencyMiddleware,
|
||||
@@ -26,6 +28,7 @@ stripeWebhookRouter.post(
|
||||
"/webhooks/connect/:env",
|
||||
stripeConnectSeederMiddleware,
|
||||
stripeWebhookRefreshMiddleware,
|
||||
stripeSyncMiddleware,
|
||||
stripeToAutumnCustomerMiddleware,
|
||||
stripeLoggerMiddleware,
|
||||
stripeIdempotencyMiddleware,
|
||||
|
||||
54
server/src/external/stripe/webhookMiddlewares/stripeSyncMiddleware.ts
vendored
Normal file
54
server/src/external/stripe/webhookMiddlewares/stripeSyncMiddleware.ts
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
import { isSyncableEvent, processStripeSyncEvent } from "@autumn/stripe-sync";
|
||||
import type { Context, Next } from "hono";
|
||||
import { isStripeSyncEnabled } from "@/internal/misc/stripeSync/stripeSyncStore.js";
|
||||
import type {
|
||||
StripeWebhookContext,
|
||||
StripeWebhookHonoEnv,
|
||||
} from "./stripeWebhookContext.js";
|
||||
|
||||
/**
|
||||
* Post-handler middleware that syncs Stripe events to the sync DB.
|
||||
* Fire-and-forget -- errors are caught and logged, never propagated.
|
||||
*/
|
||||
export const stripeSyncMiddleware = async (
|
||||
c: Context<StripeWebhookHonoEnv>,
|
||||
next: Next,
|
||||
) => {
|
||||
await next();
|
||||
|
||||
const ctx = c.get("ctx") as StripeWebhookContext;
|
||||
const { logger, org, stripeEvent } = ctx;
|
||||
|
||||
if (!org || !stripeEvent) return;
|
||||
if (
|
||||
process.env.NODE_ENV === "production" &&
|
||||
!isStripeSyncEnabled({ orgId: org.id, orgSlug: org.slug })
|
||||
)
|
||||
return;
|
||||
|
||||
if (!isSyncableEvent({ eventType: stripeEvent.type })) return;
|
||||
|
||||
try {
|
||||
const stripeAccountId = stripeEvent.account ?? undefined;
|
||||
|
||||
void processStripeSyncEvent({
|
||||
event: stripeEvent,
|
||||
stripeAccountId,
|
||||
orgId: org.id,
|
||||
env: ctx.env,
|
||||
}).catch((error) => {
|
||||
logger.error(`Stripe sync failed for event ${stripeEvent.id}: ${error}`, {
|
||||
error: {
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
},
|
||||
data: {
|
||||
eventId: stripeEvent.id,
|
||||
eventType: stripeEvent.type,
|
||||
orgId: org.id,
|
||||
},
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Stripe sync middleware error: ${error}`);
|
||||
}
|
||||
};
|
||||
@@ -20,6 +20,9 @@ import {
|
||||
import "./internal/misc/requestBlocks/requestBlockStore.js";
|
||||
import "./internal/misc/featureFlags/featureFlagStore.js";
|
||||
import "./internal/misc/customerBlocks/customerBlockStore.js";
|
||||
import "./internal/misc/edgeConfig/orgLimitsStore.js";
|
||||
import "./internal/misc/stripeSync/stripeSyncStore.js";
|
||||
import { closeStripeSyncEngine } from "@autumn/stripe-sync";
|
||||
import { warmupRegionalRedis } from "./external/redis/initRedis.js";
|
||||
import { createHonoApp } from "./initHono.js";
|
||||
import { otelSdk } from "./instrumentation.js";
|
||||
@@ -117,6 +120,7 @@ async function gracefulShutdown() {
|
||||
client.end(),
|
||||
clientCritical.end(),
|
||||
clientReplica?.end(),
|
||||
closeStripeSyncEngine(),
|
||||
]);
|
||||
console.log("Shutdown complete. Exiting process.");
|
||||
process.exit(0);
|
||||
|
||||
@@ -2,8 +2,11 @@ import { Hono } from "hono";
|
||||
import type { HonoEnv } from "../../honoUtils/HonoEnv";
|
||||
import { handleGetAdminCustomerBlockConfig } from "./handleGetAdminCustomerBlockConfig";
|
||||
import { handleGetAdminFeatureFlagsConfig } from "./handleGetAdminFeatureFlagsConfig";
|
||||
import { handleGetAdminOrgLimitsConfig } from "./handleGetAdminOrgLimitsConfig";
|
||||
import { handleGetAdminOrgRequestBlock } from "./handleGetAdminOrgRequestBlock";
|
||||
import { handleGetAdminRequestBlockConfig } from "./handleGetAdminRequestBlockConfig";
|
||||
import { handleGetAdminStripeSyncConfig } from "./handleGetAdminStripeSyncConfig";
|
||||
|
||||
import { handleGetInvoiceLineItems } from "./handleGetInvoiceLineItems";
|
||||
import { handleGetMasterStripeAccount } from "./handleGetMasterStripeAccount";
|
||||
import { handleGetOrgMember } from "./handleGetOrgMember";
|
||||
@@ -12,8 +15,10 @@ import { handleListAdminUsers } from "./handleListAdminUsers";
|
||||
import { handleListOAuthClients } from "./handleListOAuthClients";
|
||||
import { handleUpsertAdminCustomerBlockConfig } from "./handleUpsertAdminCustomerBlockConfig";
|
||||
import { handleUpsertAdminFeatureFlagsConfig } from "./handleUpsertAdminFeatureFlagsConfig";
|
||||
import { handleUpsertAdminOrgLimitsConfig } from "./handleUpsertAdminOrgLimitsConfig";
|
||||
import { handleUpsertAdminOrgRequestBlock } from "./handleUpsertAdminOrgRequestBlock";
|
||||
import { handleUpsertAdminRequestBlockConfig } from "./handleUpsertAdminRequestBlockConfig";
|
||||
import { handleUpsertAdminStripeSyncConfig } from "./handleUpsertAdminStripeSyncConfig";
|
||||
|
||||
export const honoAdminRouter = new Hono<HonoEnv>();
|
||||
|
||||
@@ -51,6 +56,13 @@ honoAdminRouter.put(
|
||||
"/customer-block-config",
|
||||
...handleUpsertAdminCustomerBlockConfig,
|
||||
);
|
||||
honoAdminRouter.get("/org-limits-config", ...handleGetAdminOrgLimitsConfig);
|
||||
honoAdminRouter.put("/org-limits-config", ...handleUpsertAdminOrgLimitsConfig);
|
||||
honoAdminRouter.get("/stripe-sync-config", ...handleGetAdminStripeSyncConfig);
|
||||
honoAdminRouter.put(
|
||||
"/stripe-sync-config",
|
||||
...handleUpsertAdminStripeSyncConfig,
|
||||
);
|
||||
honoAdminRouter.get("/org-member", ...handleGetOrgMember);
|
||||
honoAdminRouter.get("/master-stripe-account", ...handleGetMasterStripeAccount);
|
||||
honoAdminRouter.get("/oauth-clients", ...handleListOAuthClients);
|
||||
|
||||
20
server/src/internal/admin/handleGetAdminOrgLimitsConfig.ts
Normal file
20
server/src/internal/admin/handleGetAdminOrgLimitsConfig.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||
import {
|
||||
getOrgLimitsConfigFromSource,
|
||||
getRuntimeOrgLimitsStatus,
|
||||
} from "@/internal/misc/edgeConfig/orgLimitsStore.js";
|
||||
|
||||
export const handleGetAdminOrgLimitsConfig = createRoute({
|
||||
handler: async (c) => {
|
||||
const status = getRuntimeOrgLimitsStatus();
|
||||
const config = await getOrgLimitsConfigFromSource();
|
||||
|
||||
return c.json({
|
||||
...config,
|
||||
configHealthy: status.healthy,
|
||||
configConfigured: status.configured,
|
||||
lastSuccessAt: status.lastSuccessAt ?? null,
|
||||
error: status.error ?? null,
|
||||
});
|
||||
},
|
||||
});
|
||||
20
server/src/internal/admin/handleGetAdminStripeSyncConfig.ts
Normal file
20
server/src/internal/admin/handleGetAdminStripeSyncConfig.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||
import {
|
||||
getRuntimeStripeSyncStatus,
|
||||
getStripeSyncConfigFromSource,
|
||||
} from "@/internal/misc/stripeSync/stripeSyncStore.js";
|
||||
|
||||
export const handleGetAdminStripeSyncConfig = createRoute({
|
||||
handler: async (c) => {
|
||||
const status = getRuntimeStripeSyncStatus();
|
||||
const config = await getStripeSyncConfigFromSource();
|
||||
|
||||
return c.json({
|
||||
...config,
|
||||
configHealthy: status.healthy,
|
||||
configConfigured: status.configured,
|
||||
lastSuccessAt: status.lastSuccessAt ?? null,
|
||||
error: status.error ?? null,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||
import { OrgLimitsConfigSchema } from "@/internal/misc/edgeConfig/orgLimitsSchemas.js";
|
||||
import { updateFullOrgLimitsConfig } from "@/internal/misc/edgeConfig/orgLimitsStore.js";
|
||||
|
||||
export const handleUpsertAdminOrgLimitsConfig = createRoute({
|
||||
body: OrgLimitsConfigSchema,
|
||||
handler: async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
|
||||
await updateFullOrgLimitsConfig({ config: body });
|
||||
|
||||
return c.json({ success: true });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createRoute } from "@/honoMiddlewares/routeHandler.js";
|
||||
import { StripeSyncConfigSchema } from "@/internal/misc/stripeSync/stripeSyncSchemas.js";
|
||||
import { updateFullStripeSyncConfig } from "@/internal/misc/stripeSync/stripeSyncStore.js";
|
||||
|
||||
export const handleUpsertAdminStripeSyncConfig = createRoute({
|
||||
body: StripeSyncConfigSchema,
|
||||
handler: async (c) => {
|
||||
const body = c.req.valid("json");
|
||||
|
||||
await updateFullStripeSyncConfig({ config: body });
|
||||
|
||||
return c.json({ success: true });
|
||||
},
|
||||
});
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@autumn/shared";
|
||||
import * as Sentry from "@sentry/bun";
|
||||
import type { AutumnContext, RequestContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { getOrgCusProductLimit } from "../misc/edgeConfig/orgLimitsStore.js";
|
||||
import { triggerBatchResetCustomerEntitlements } from "./actions/resetCustomerEntitlements/triggerBatchResetCustomerEntitlements.js";
|
||||
import { getApiCustomerBase } from "./cusUtils/apiCusUtils/getApiCustomerBase.js";
|
||||
import { getPaginatedFullCusQuery } from "./getFullCusQuery.js";
|
||||
@@ -24,6 +25,11 @@ export class CusBatchService {
|
||||
internalCustomerIds: string[];
|
||||
}) {
|
||||
const { org, env, db } = ctx;
|
||||
const cusProductLimit = getOrgCusProductLimit({
|
||||
orgId: ctx.org.id,
|
||||
orgSlug: ctx.org.slug,
|
||||
});
|
||||
|
||||
const query = getPaginatedFullCusQuery({
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
@@ -34,6 +40,7 @@ export class CusBatchService {
|
||||
limit: internalCustomerIds.length || 100,
|
||||
offset: 0,
|
||||
internalCustomerIds,
|
||||
cusProductLimit,
|
||||
});
|
||||
const results = await db.execute(query);
|
||||
const fullCustomers = results as unknown as FullCustomer[];
|
||||
@@ -66,6 +73,10 @@ export class CusBatchService {
|
||||
|
||||
const { limit, offset, plans, subscription_status, search } = query;
|
||||
|
||||
const cusProductLimit = getOrgCusProductLimit({
|
||||
orgId: ctx.org.id,
|
||||
orgSlug: ctx.org.slug,
|
||||
});
|
||||
const sqlQuery = getPaginatedFullCusQuery({
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
@@ -80,6 +91,7 @@ export class CusBatchService {
|
||||
offset,
|
||||
search,
|
||||
plans,
|
||||
cusProductLimit,
|
||||
});
|
||||
const results = await ctx.db.execute(sqlQuery);
|
||||
const finals = [];
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
|
||||
import {
|
||||
and,
|
||||
asc,
|
||||
desc,
|
||||
eq,
|
||||
gt,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
} from "drizzle-orm";
|
||||
import { alias } from "drizzle-orm/pg-core";
|
||||
import type { DrizzleCli } from "@/db/initDrizzle.js";
|
||||
import { getOrgCusProductLimit } from "../misc/edgeConfig/orgLimitsStore.js";
|
||||
|
||||
// Create alias for subquery
|
||||
const customerProductsAlias = alias(customerProducts, "cp_alias");
|
||||
@@ -47,6 +49,7 @@ const productFields = {
|
||||
id: products.id,
|
||||
name: products.name,
|
||||
version: products.version,
|
||||
is_add_on: products.is_add_on,
|
||||
};
|
||||
|
||||
interface SearchFilters {
|
||||
@@ -60,25 +63,21 @@ export class CusSearchService {
|
||||
static async searchByProduct({
|
||||
db,
|
||||
orgId,
|
||||
orgSlug,
|
||||
env,
|
||||
search,
|
||||
filters,
|
||||
pageSize = 50,
|
||||
pageNumber,
|
||||
// lastItem,
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
orgId: string;
|
||||
orgSlug?: string;
|
||||
env: AppEnv;
|
||||
search: string;
|
||||
filters: SearchFilters;
|
||||
pageSize?: number;
|
||||
pageNumber: number;
|
||||
// lastItem?: {
|
||||
// internal_id: string;
|
||||
// created_at?: string;
|
||||
// name?: string;
|
||||
// } | null;
|
||||
}) {
|
||||
// If we have a lastItem with only internal_id, fetch the full customer data for cursor pagination
|
||||
// let resolvedLastItem = lastItem;
|
||||
@@ -304,14 +303,14 @@ export class CusSearchService {
|
||||
const offset = (pageNumber - 1) * pageSize;
|
||||
productQueryResult = buildQuery()
|
||||
.where(whereClause)
|
||||
.orderBy(desc(customers.internal_id))
|
||||
.orderBy(desc(customers.internal_id), asc(products.is_add_on))
|
||||
.offset(offset)
|
||||
.limit(pageSize);
|
||||
} else {
|
||||
// Use cursor-based pagination
|
||||
productQueryResult = buildQuery()
|
||||
.where(whereClause)
|
||||
.orderBy(desc(customers.internal_id))
|
||||
.orderBy(desc(customers.internal_id), asc(products.is_add_on))
|
||||
.limit(pageSize);
|
||||
}
|
||||
|
||||
@@ -375,7 +374,14 @@ export class CusSearchService {
|
||||
}
|
||||
}
|
||||
|
||||
const cusProductLimit = getOrgCusProductLimit({ orgId, orgSlug });
|
||||
const processedData = Array.from(customerMap.values());
|
||||
for (const customer of processedData) {
|
||||
customer.customer_products = customer.customer_products.slice(
|
||||
0,
|
||||
cusProductLimit,
|
||||
);
|
||||
}
|
||||
|
||||
const totalCount = totalCountResult[0]?.totalCount || 0;
|
||||
|
||||
@@ -495,9 +501,7 @@ export class CusSearchService {
|
||||
return sql`(${customers.processors}->>'vercel' IS NOT NULL)`;
|
||||
return undefined;
|
||||
})
|
||||
.filter(
|
||||
(c): c is NonNullable<typeof c> => c !== undefined,
|
||||
),
|
||||
.filter((c): c is NonNullable<typeof c> => c !== undefined),
|
||||
)
|
||||
: undefined,
|
||||
),
|
||||
@@ -510,6 +514,7 @@ export class CusSearchService {
|
||||
static async search({
|
||||
db,
|
||||
orgId,
|
||||
orgSlug,
|
||||
env,
|
||||
search,
|
||||
pageSize = 50,
|
||||
@@ -519,6 +524,7 @@ export class CusSearchService {
|
||||
}: {
|
||||
db: DrizzleCli;
|
||||
orgId: string;
|
||||
orgSlug?: string;
|
||||
env: AppEnv;
|
||||
search: string;
|
||||
lastItem?: {
|
||||
@@ -570,7 +576,6 @@ export class CusSearchService {
|
||||
search,
|
||||
filters,
|
||||
pageSize,
|
||||
// lastItem: resolvedLastItem,
|
||||
pageNumber,
|
||||
});
|
||||
}
|
||||
@@ -583,11 +588,11 @@ export class CusSearchService {
|
||||
return await CusSearchService.searchByProduct({
|
||||
db,
|
||||
orgId,
|
||||
orgSlug,
|
||||
env,
|
||||
search,
|
||||
filters,
|
||||
pageSize,
|
||||
// lastItem: resolvedLastItem,
|
||||
pageNumber,
|
||||
});
|
||||
}
|
||||
@@ -685,7 +690,7 @@ export class CusSearchService {
|
||||
products,
|
||||
eq(customerProducts.internal_product_id, products.internal_id),
|
||||
)
|
||||
.orderBy(desc(baseQuery.internal_id)),
|
||||
.orderBy(desc(baseQuery.internal_id), asc(products.is_add_on)),
|
||||
totalCountQuery,
|
||||
]);
|
||||
|
||||
@@ -718,7 +723,14 @@ export class CusSearchService {
|
||||
}
|
||||
}
|
||||
|
||||
const cusProductLimit = getOrgCusProductLimit({ orgId, orgSlug });
|
||||
const finalResults = Array.from(customerMap.values());
|
||||
for (const customer of finalResults) {
|
||||
customer.customer_products = customer.customer_products.slice(
|
||||
0,
|
||||
cusProductLimit,
|
||||
);
|
||||
}
|
||||
|
||||
return { data: finalResults, count: totalCount };
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
CustomerNotFoundError,
|
||||
customerProducts,
|
||||
customers,
|
||||
type Entity,
|
||||
type EntityExpand,
|
||||
ErrCode,
|
||||
type FullCusProduct,
|
||||
@@ -33,6 +34,7 @@ import { executeWithHealthTracking } from "@/db/pgHealthMonitor.js";
|
||||
import type { RepoContext } from "@/db/repoContext.js";
|
||||
import type { AutumnContext } from "@/honoUtils/HonoEnv.js";
|
||||
import { withSpan } from "../analytics/tracer/spanUtils.js";
|
||||
import { getOrgCusProductLimit } from "../misc/edgeConfig/orgLimitsStore.js";
|
||||
import { resetCustomerEntitlements } from "./actions/resetCustomerEntitlements/resetCustomerEntitlements.js";
|
||||
import {
|
||||
ACTIVE_STATUSES,
|
||||
@@ -86,7 +88,11 @@ export class CusService {
|
||||
withSubs,
|
||||
},
|
||||
fn: async () => {
|
||||
const query = getFullCusQuery(
|
||||
const cusProductLimit = getOrgCusProductLimit({
|
||||
orgId,
|
||||
orgSlug: org.slug,
|
||||
});
|
||||
const query = getFullCusQuery({
|
||||
idOrInternalId,
|
||||
orgId,
|
||||
env,
|
||||
@@ -97,7 +103,8 @@ export class CusService {
|
||||
withSubs,
|
||||
withEvents,
|
||||
entityId,
|
||||
);
|
||||
cusProductLimit,
|
||||
});
|
||||
|
||||
if (explain) {
|
||||
const explainQuery = sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) ${query}`;
|
||||
@@ -144,10 +151,18 @@ export class CusService {
|
||||
.slice(0, 5);
|
||||
}
|
||||
|
||||
// Skip reset when reading from replica — it writes to primary,
|
||||
// and replica data is stale anyway. When degraded WITHOUT a replica
|
||||
// (falls back to primary), the reset should still run.
|
||||
if (orgId === "GG6tnmO7cHb40PNhwYBTZtxQdeL74NHF") {
|
||||
fullCus.customer_products = (
|
||||
fullCus.customer_products as FullCusProduct[]
|
||||
)
|
||||
.sort((a, b) => b.customer_prices.length - a.customer_prices.length)
|
||||
.slice(0, 5);
|
||||
|
||||
fullCus.entities = (fullCus.entities as Entity[]).slice(0, 50);
|
||||
}
|
||||
if (!usedReplica && !skipReset) {
|
||||
// Skip reset only when executeWithHealthTracking explicitly chose the
|
||||
// replica. Lazy reset writes themselves go through dbGeneral.
|
||||
await resetCustomerEntitlements({
|
||||
fullCus,
|
||||
ctx,
|
||||
@@ -318,22 +333,21 @@ export class CusService {
|
||||
? [query.subscription_status as CusProductStatus]
|
||||
: ACTIVE_STATUSES;
|
||||
|
||||
const processorFilter =
|
||||
query?.processors?.length
|
||||
? or(
|
||||
...query.processors
|
||||
.map((proc) => {
|
||||
if (proc === "stripe")
|
||||
return sql`(${customers.processor}->>'id' IS NOT NULL)`;
|
||||
if (proc === "revenuecat")
|
||||
return sql`(${customers.processors}->>'revenuecat' IS NOT NULL)`;
|
||||
if (proc === "vercel")
|
||||
return sql`(${customers.processors}->>'vercel' IS NOT NULL)`;
|
||||
return undefined;
|
||||
})
|
||||
.filter((c): c is NonNullable<typeof c> => c !== undefined),
|
||||
)
|
||||
: undefined;
|
||||
const processorFilter = query?.processors?.length
|
||||
? or(
|
||||
...query.processors
|
||||
.map((proc) => {
|
||||
if (proc === "stripe")
|
||||
return sql`(${customers.processor}->>'id' IS NOT NULL)`;
|
||||
if (proc === "revenuecat")
|
||||
return sql`(${customers.processors}->>'revenuecat' IS NOT NULL)`;
|
||||
if (proc === "vercel")
|
||||
return sql`(${customers.processors}->>'vercel' IS NOT NULL)`;
|
||||
return undefined;
|
||||
})
|
||||
.filter((c): c is NonNullable<typeof c> => c !== undefined),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!query?.plans?.length) {
|
||||
const [result] = await ctx.db
|
||||
|
||||
@@ -8,10 +8,27 @@ import { executeBillingPlan } from "@/internal/billing/v2/execute/executeBilling
|
||||
import { evaluateStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/actionBuilders/evaluateStripeBillingPlan";
|
||||
import { logStripeBillingPlan } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingPlan";
|
||||
import { logStripeBillingResult } from "@/internal/billing/v2/providers/stripe/logs/logStripeBillingResult";
|
||||
import { buildBillingLockKey } from "@/internal/billing/v2/utils/billingLock/buildBillingLockKey";
|
||||
import { logAutumnBillingPlan } from "@/internal/billing/v2/utils/logs/logAutumnBillingPlan";
|
||||
|
||||
export const handleCancelV2 = createRoute({
|
||||
// body: CancelBodySchema,
|
||||
lock:
|
||||
process.env.NODE_ENV !== "development"
|
||||
? {
|
||||
ttlMs: 120000,
|
||||
errorMessage:
|
||||
"Cancel already in progress for this customer, try again in a few seconds",
|
||||
getKey: (c) => {
|
||||
const ctx = c.get("ctx");
|
||||
if (!ctx.customerId) return null;
|
||||
return buildBillingLockKey({
|
||||
orgId: ctx.org.id,
|
||||
env: ctx.env,
|
||||
customerId: ctx.customerId,
|
||||
});
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
handler: async (c) => {
|
||||
const ctx = c.get("ctx");
|
||||
|
||||
|
||||
@@ -149,6 +149,10 @@ export const getCachedFullCustomer = async ({
|
||||
data: JSON.parse(cached),
|
||||
});
|
||||
|
||||
// if (fullCustomer.entities?.[0] === null) {
|
||||
// fullCustomer.entities = fullCustomer.entities?.filter((e) => e !== null);
|
||||
// }
|
||||
|
||||
if (entityId) {
|
||||
fullCustomer.entity = fullCustomer.entities?.find((e) => e.id === entityId);
|
||||
} else {
|
||||
|
||||
@@ -6,7 +6,13 @@ import {
|
||||
} from "@autumn/shared";
|
||||
import { type SQL, sql } from "drizzle-orm";
|
||||
|
||||
const buildOptimizedCusProductsCTE = (inStatuses?: CusProductStatus[]) => {
|
||||
const buildOptimizedCusProductsCTE = ({
|
||||
inStatuses,
|
||||
cusProductLimit,
|
||||
}: {
|
||||
inStatuses?: CusProductStatus[];
|
||||
cusProductLimit: number;
|
||||
}) => {
|
||||
const withStatusFilter = () => {
|
||||
return inStatuses
|
||||
? sql`AND cp.status = ANY(ARRAY[${sql.join(
|
||||
@@ -86,8 +92,8 @@ const buildOptimizedCusProductsCTE = (inStatuses?: CusProductStatus[]) => {
|
||||
) ft_data ON true
|
||||
WHERE cp.internal_customer_id = (SELECT internal_id FROM customer_record)
|
||||
${withStatusFilter()}
|
||||
ORDER BY ${relevantStatusFirst}, cp.created_at DESC
|
||||
LIMIT 15
|
||||
ORDER BY ${relevantStatusFirst}, prod.is_add_on ASC, cp.created_at DESC
|
||||
LIMIT ${cusProductLimit}
|
||||
)
|
||||
`;
|
||||
};
|
||||
@@ -101,7 +107,7 @@ const buildEntitiesCTE = (withEntities: boolean) => {
|
||||
customer_entities AS (
|
||||
SELECT
|
||||
COALESCE(
|
||||
json_agg(row_to_json(e) ORDER BY e.internal_id DESC),
|
||||
json_agg(row_to_json(e) ORDER BY e.internal_id DESC) FILTER (WHERE e.internal_id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS entities
|
||||
FROM (
|
||||
@@ -266,18 +272,31 @@ const buildInvoicesCTE = (hasEntityCTE: boolean) => {
|
||||
`;
|
||||
};
|
||||
|
||||
export const getFullCusQuery = (
|
||||
idOrInternalId: string,
|
||||
orgId: string,
|
||||
env: AppEnv,
|
||||
inStatuses: CusProductStatus[],
|
||||
includeInvoices: boolean,
|
||||
withEntities: boolean,
|
||||
withTrialsUsed: boolean,
|
||||
withSubs: boolean,
|
||||
withEvents: boolean,
|
||||
entityId?: string,
|
||||
) => {
|
||||
export const getFullCusQuery = ({
|
||||
idOrInternalId,
|
||||
orgId,
|
||||
env,
|
||||
inStatuses,
|
||||
includeInvoices,
|
||||
withEntities,
|
||||
withTrialsUsed,
|
||||
withSubs,
|
||||
withEvents,
|
||||
entityId,
|
||||
cusProductLimit,
|
||||
}: {
|
||||
idOrInternalId: string;
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
inStatuses: CusProductStatus[];
|
||||
includeInvoices: boolean;
|
||||
withEntities: boolean;
|
||||
withTrialsUsed: boolean;
|
||||
withSubs: boolean;
|
||||
withEvents: boolean;
|
||||
entityId?: string;
|
||||
cusProductLimit: number;
|
||||
}) => {
|
||||
const sqlChunks: SQL[] = [];
|
||||
|
||||
// Step 1: Get customer record
|
||||
@@ -309,7 +328,7 @@ export const getFullCusQuery = (
|
||||
// Add customer products CTE
|
||||
sqlChunks.push(sql`, `);
|
||||
// sqlChunks.push(buildCusProductsCTE(inStatuses));
|
||||
sqlChunks.push(buildOptimizedCusProductsCTE(inStatuses));
|
||||
sqlChunks.push(buildOptimizedCusProductsCTE({ inStatuses, cusProductLimit }));
|
||||
|
||||
// Conditionally add trials used CTE
|
||||
if (withTrialsUsed) {
|
||||
@@ -429,6 +448,7 @@ export const getPaginatedFullCusQuery = ({
|
||||
internalCustomerIds,
|
||||
plans,
|
||||
search,
|
||||
cusProductLimit,
|
||||
}: {
|
||||
orgId: string;
|
||||
env: AppEnv;
|
||||
@@ -444,6 +464,7 @@ export const getPaginatedFullCusQuery = ({
|
||||
internalCustomerIds?: string[];
|
||||
plans?: ListCustomersV2Params["plans"];
|
||||
search?: string;
|
||||
cusProductLimit: number;
|
||||
}) => {
|
||||
const withStatusFilter = () => {
|
||||
return inStatuses?.length
|
||||
@@ -539,8 +560,8 @@ export const getPaginatedFullCusQuery = ({
|
||||
FROM customer_products cp
|
||||
WHERE cp.internal_customer_id = cr.internal_id
|
||||
${withStatusFilter()}
|
||||
ORDER BY cp.created_at DESC
|
||||
LIMIT 15
|
||||
ORDER BY (SELECT p.is_add_on FROM products p WHERE p.internal_id = cp.internal_product_id) ASC, cp.created_at DESC
|
||||
LIMIT ${cusProductLimit}
|
||||
) cp ON true
|
||||
JOIN products prod ON cp.internal_product_id = prod.internal_id
|
||||
LEFT JOIN LATERAL (
|
||||
@@ -659,7 +680,7 @@ export const getPaginatedFullCusQuery = ({
|
||||
SELECT
|
||||
cr.internal_id AS internal_customer_id,
|
||||
COALESCE(
|
||||
json_agg(row_to_json(e) ORDER BY e.internal_id DESC),
|
||||
json_agg(row_to_json(e) ORDER BY e.internal_id DESC) FILTER (WHERE e.internal_id IS NOT NULL),
|
||||
'[]'::json
|
||||
) AS entities
|
||||
FROM customer_records cr
|
||||
|
||||
@@ -24,6 +24,7 @@ export const handleSearchCustomers = createRoute({
|
||||
const { data: customers, count } = await CusSearchService.search({
|
||||
db,
|
||||
orgId: org.id,
|
||||
orgSlug: org.slug,
|
||||
env,
|
||||
search: search ?? "",
|
||||
filters,
|
||||
|
||||
14
server/src/internal/misc/edgeConfig/orgLimitsSchemas.ts
Normal file
14
server/src/internal/misc/edgeConfig/orgLimitsSchemas.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const OrgLimitsConfigSchema = z.object({
|
||||
orgs: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
maxCusProducts: z.number().min(1).optional(),
|
||||
}),
|
||||
)
|
||||
.default({}),
|
||||
});
|
||||
|
||||
export type OrgLimitsConfig = z.infer<typeof OrgLimitsConfigSchema>;
|
||||
65
server/src/internal/misc/edgeConfig/orgLimitsStore.ts
Normal file
65
server/src/internal/misc/edgeConfig/orgLimitsStore.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ADMIN_ORG_LIMITS_CONFIG_KEY } from "@/external/aws/s3/adminS3Config.js";
|
||||
import { registerEdgeConfig } from "@/internal/misc/edgeConfig/edgeConfigRegistry.js";
|
||||
import { createEdgeConfigStore } from "@/internal/misc/edgeConfig/edgeConfigStore.js";
|
||||
import {
|
||||
type OrgLimitsConfig,
|
||||
OrgLimitsConfigSchema,
|
||||
} from "./orgLimitsSchemas.js";
|
||||
|
||||
export const DEFAULT_CUS_PRODUCT_LIMIT = 15;
|
||||
|
||||
const store = createEdgeConfigStore<OrgLimitsConfig>({
|
||||
s3Key: ADMIN_ORG_LIMITS_CONFIG_KEY,
|
||||
schema: OrgLimitsConfigSchema,
|
||||
defaultValue: () => ({ orgs: {} }),
|
||||
pollIntervalMs: 30_000,
|
||||
});
|
||||
|
||||
registerEdgeConfig({ store });
|
||||
|
||||
export const getRuntimeOrgLimitsStatus = () => store.getStatus();
|
||||
|
||||
export const getOrgCusProductLimit = ({
|
||||
orgId,
|
||||
orgSlug,
|
||||
}: {
|
||||
orgId?: string;
|
||||
orgSlug?: string;
|
||||
}): number => {
|
||||
const orgs = store.get().orgs;
|
||||
const orgConfig =
|
||||
(orgId ? orgs[orgId] : undefined) ??
|
||||
(orgSlug ? orgs[orgSlug] : undefined);
|
||||
return orgConfig?.maxCusProducts ?? DEFAULT_CUS_PRODUCT_LIMIT;
|
||||
};
|
||||
|
||||
export const getOrgLimitsConfigFromSource = async () => {
|
||||
return await store.readFromSource();
|
||||
};
|
||||
|
||||
export const updateOrgLimitsInSource = async ({
|
||||
orgId,
|
||||
maxCusProducts,
|
||||
}: {
|
||||
orgId: string;
|
||||
maxCusProducts?: number;
|
||||
}) => {
|
||||
const config = await store.readFromSource();
|
||||
|
||||
if (maxCusProducts === undefined) {
|
||||
delete config.orgs[orgId];
|
||||
} else {
|
||||
config.orgs[orgId] = { maxCusProducts };
|
||||
}
|
||||
|
||||
await store.writeToSource({ config });
|
||||
return config.orgs[orgId];
|
||||
};
|
||||
|
||||
export const updateFullOrgLimitsConfig = async ({
|
||||
config,
|
||||
}: {
|
||||
config: OrgLimitsConfig;
|
||||
}) => {
|
||||
await store.writeToSource({ config });
|
||||
};
|
||||
7
server/src/internal/misc/stripeSync/stripeSyncSchemas.ts
Normal file
7
server/src/internal/misc/stripeSync/stripeSyncSchemas.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const StripeSyncConfigSchema = z.object({
|
||||
enabledOrgIds: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
export type StripeSyncConfig = z.infer<typeof StripeSyncConfigSchema>;
|
||||
39
server/src/internal/misc/stripeSync/stripeSyncStore.ts
Normal file
39
server/src/internal/misc/stripeSync/stripeSyncStore.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { registerEdgeConfig } from "@/internal/misc/edgeConfig/edgeConfigRegistry.js";
|
||||
import { createEdgeConfigStore } from "@/internal/misc/edgeConfig/edgeConfigStore.js";
|
||||
import {
|
||||
type StripeSyncConfig,
|
||||
StripeSyncConfigSchema,
|
||||
} from "./stripeSyncSchemas.js";
|
||||
|
||||
const store = createEdgeConfigStore<StripeSyncConfig>({
|
||||
s3Key: "admin/stripe-sync-config.json",
|
||||
schema: StripeSyncConfigSchema,
|
||||
defaultValue: () => ({ enabledOrgIds: [] }),
|
||||
pollIntervalMs: 60_000,
|
||||
});
|
||||
|
||||
registerEdgeConfig({ store });
|
||||
|
||||
/** Pure in-memory lookup -- zero I/O, sync. Matches on org ID or slug. */
|
||||
export const isStripeSyncEnabled = ({
|
||||
orgId,
|
||||
orgSlug,
|
||||
}: {
|
||||
orgId: string;
|
||||
orgSlug?: string;
|
||||
}): boolean => {
|
||||
const enabled = store.get().enabledOrgIds;
|
||||
return enabled.includes(orgId) || (!!orgSlug && enabled.includes(orgSlug));
|
||||
};
|
||||
|
||||
export const getRuntimeStripeSyncStatus = () => store.getStatus();
|
||||
|
||||
export const getStripeSyncConfigFromSource = async () => store.readFromSource();
|
||||
|
||||
export const updateFullStripeSyncConfig = async ({
|
||||
config,
|
||||
}: {
|
||||
config: StripeSyncConfig;
|
||||
}) => {
|
||||
await store.writeToSource({ config });
|
||||
};
|
||||
@@ -1,67 +1,106 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { test } from "bun:test";
|
||||
import type { AttachParamsV1Input } from "@autumn/shared";
|
||||
import {
|
||||
applySubscriptionDiscount,
|
||||
createPercentCoupon,
|
||||
getStripeSubscription,
|
||||
} from "@tests/integration/billing/utils/discounts/discountTestUtils";
|
||||
import { TestFeature } from "@tests/setup/v2Features.js";
|
||||
import { items } from "@tests/utils/fixtures/items";
|
||||
import { products } from "@tests/utils/fixtures/products";
|
||||
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario";
|
||||
import chalk from "chalk";
|
||||
import { CusService } from "@/internal/customers/CusService";
|
||||
|
||||
test(`${chalk.yellowBright("check where default payment method lives after attach")}`, async () => {
|
||||
const customerId = "pm-location-check";
|
||||
/**
|
||||
* Repro for Mintlify multi-entity discount bug.
|
||||
*
|
||||
* Production sequence (from Axiom logs):
|
||||
* 1. Entity 1 attaches pro (upgrade from Hobby) — creates subscription
|
||||
* 2. Discount applied to subscription
|
||||
* 3. Entity 1 cancels pro (end_of_cycle) — creates subscription schedule
|
||||
* 4. Entity 1 uncancels pro — schedule modified
|
||||
* 5. Entity 2 attaches pro — subscription update succeeds but schedule
|
||||
* creation/update fails: "Discount di_xxx has exceeded its maximum
|
||||
* number of applications and cannot be reused"
|
||||
*
|
||||
* The error is in stripeDiscountsToPhaseDiscounts which passes
|
||||
* { discount: "di_xxx" } to schedule phases — but the discount is bound
|
||||
* to the subscription, not the schedule.
|
||||
*/
|
||||
test(`${chalk.yellowBright("bug repro: entity 2 attach after cancel+uncancel with discount on sub")}`, async () => {
|
||||
const customerId = "multi-ent-cancel-uncancel";
|
||||
|
||||
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
||||
|
||||
const pro = products.pro({
|
||||
id: "pro-pm-check",
|
||||
id: "pro",
|
||||
items: [messagesItem],
|
||||
});
|
||||
|
||||
const { ctx } = await initScenario({
|
||||
// Step 1: Entity 1 attaches pro
|
||||
const { autumnV2_2, autumnV1, entities } = await initScenario({
|
||||
customerId,
|
||||
setup: [
|
||||
s.customer({ paymentMethod: "success" }),
|
||||
s.products({ list: [pro] }),
|
||||
s.entities({ count: 2, featureId: TestFeature.Users }),
|
||||
],
|
||||
actions: [
|
||||
s.billing.attach({ productId: pro.id, entityIndex: 0 }),
|
||||
],
|
||||
actions: [s.attach({ productId: pro.id })],
|
||||
});
|
||||
|
||||
const fullCustomer = await CusService.getFull({
|
||||
ctx,
|
||||
idOrInternalId: customerId,
|
||||
// Step 2: Apply a discount to entity 1's subscription
|
||||
const { stripeCli, subscription } = await getStripeSubscription({
|
||||
customerId,
|
||||
});
|
||||
|
||||
const stripeCustomerId = fullCustomer.processor?.id;
|
||||
expect(stripeCustomerId).toBeDefined();
|
||||
|
||||
const subs = await ctx.stripeCli.subscriptions.list({
|
||||
customer: stripeCustomerId!,
|
||||
status: "all",
|
||||
const coupon = await createPercentCoupon({
|
||||
stripeCli,
|
||||
percentOff: 10,
|
||||
});
|
||||
|
||||
const subscription = subs.data.find(
|
||||
(sub) => sub.status === "active" || sub.status === "trialing",
|
||||
);
|
||||
expect(subscription).toBeDefined();
|
||||
await applySubscriptionDiscount({
|
||||
stripeCli,
|
||||
subscriptionId: subscription.id,
|
||||
couponIds: [coupon.id],
|
||||
});
|
||||
|
||||
const subDefaultPm = subscription!.default_payment_method;
|
||||
console.log(`Subscription default_payment_method: ${JSON.stringify(subDefaultPm)}`);
|
||||
// Step 3: Entity 1 cancels pro (end_of_cycle) — creates subscription schedule
|
||||
console.log("Canceling entity 1 pro (end of cycle)...");
|
||||
await autumnV2_2.subscriptions.update({
|
||||
customer_id: customerId,
|
||||
entity_id: entities[0].id,
|
||||
plan_id: pro.id,
|
||||
cancel_action: "cancel_end_of_cycle",
|
||||
});
|
||||
|
||||
const stripeCustomer = await ctx.stripeCli.customers.retrieve(stripeCustomerId!);
|
||||
const customerDefaultPm = "deleted" in stripeCustomer
|
||||
? null
|
||||
: stripeCustomer.invoice_settings?.default_payment_method;
|
||||
console.log(`Customer invoice_settings.default_payment_method: ${JSON.stringify(customerDefaultPm)}`);
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
const customerDefaultSource = "deleted" in stripeCustomer
|
||||
? null
|
||||
: stripeCustomer.default_source;
|
||||
console.log(`Customer default_source: ${JSON.stringify(customerDefaultSource)}`);
|
||||
// Step 4: Entity 1 uncancels pro — schedule released/modified
|
||||
console.log("Uncanceling entity 1 pro...");
|
||||
await autumnV2_2.subscriptions.update({
|
||||
customer_id: customerId,
|
||||
entity_id: entities[0].id,
|
||||
plan_id: pro.id,
|
||||
cancel_action: "uncancel",
|
||||
});
|
||||
|
||||
if (subDefaultPm) {
|
||||
console.log(chalk.green("Payment method is set at the SUBSCRIPTION level"));
|
||||
} else if (customerDefaultPm) {
|
||||
console.log(chalk.green("Payment method is set at the CUSTOMER level (invoice_settings)"));
|
||||
} else if (customerDefaultSource) {
|
||||
console.log(chalk.green("Payment method is set at the CUSTOMER level (default_source)"));
|
||||
} else {
|
||||
console.log(chalk.red("No default payment method found on subscription or customer"));
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
|
||||
// Step 5: Entity 2 attaches pro
|
||||
console.log("Attaching pro to entity 2...");
|
||||
try {
|
||||
const result = await autumnV2_2.billing.attach<AttachParamsV1Input>({
|
||||
customer_id: customerId,
|
||||
entity_id: entities[1].id,
|
||||
plan_id: pro.id,
|
||||
redirect_mode: "if_required",
|
||||
});
|
||||
console.log("Entity 2 result:", JSON.stringify(result, null, 2));
|
||||
console.log(chalk.red("BUG NOT REPRODUCED — entity 2 attach succeeded"));
|
||||
} catch (error: any) {
|
||||
console.log(chalk.green("BUG REPRODUCED — entity 2 attach failed:"));
|
||||
console.log("Error:", JSON.stringify(error, null, 2));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { test } from "bun:test";
|
||||
import type { ApiCustomerV3 } from "@autumn/shared";
|
||||
import {
|
||||
applySubscriptionDiscount,
|
||||
createPercentCoupon,
|
||||
getStripeSubscription,
|
||||
} from "@tests/integration/billing/utils/discounts/discountTestUtils";
|
||||
import { expectCustomerFeatureCorrect } from "@tests/integration/billing/utils/expectCustomerFeatureCorrect";
|
||||
import { expectCustomerInvoiceCorrect } from "@tests/integration/billing/utils/expectCustomerInvoiceCorrect";
|
||||
import {
|
||||
expectProductCanceling,
|
||||
expectProductScheduled,
|
||||
} from "@tests/integration/billing/utils/expectCustomerProductCorrect";
|
||||
import { TestFeature } from "@tests/setup/v2Features";
|
||||
import { items } from "@tests/utils/fixtures/items.js";
|
||||
import { products } from "@tests/utils/fixtures/products.js";
|
||||
import { initScenario, s } from "@tests/utils/testInitUtils/initScenario.js";
|
||||
import chalk from "chalk";
|
||||
|
||||
/**
|
||||
* Reproduces: attaching a one-off add-on to a subscription that already
|
||||
* has a schedule (from a prior downgrade) fails with Stripe error
|
||||
* "You cannot migrate a subscription that is already attached to a schedule".
|
||||
*
|
||||
* The root cause is that setupStripeBillingContext skips fetching the schedule
|
||||
* when targetCustomerProduct is undefined (new attachment, no transition).
|
||||
* buildStripeSubscriptionScheduleAction then sees hasSchedule=false and emits
|
||||
* type:"create" instead of type:"update", which Stripe rejects.
|
||||
*/
|
||||
test.concurrent(`${chalk.yellowBright("bug-repro: attach one-off add-on after scheduled downgrade hits 'subscription already attached to schedule'")}`, async () => {
|
||||
const customerId = "repro-schedule-addon-attach";
|
||||
|
||||
const messagesItem = items.monthlyMessages({ includedUsage: 100 });
|
||||
|
||||
const premium = products.premium({
|
||||
id: "premium",
|
||||
items: [messagesItem],
|
||||
});
|
||||
|
||||
const pro = products.pro({
|
||||
id: "pro",
|
||||
items: [messagesItem],
|
||||
});
|
||||
|
||||
const topupAddon = products.base({
|
||||
id: "topup-addon",
|
||||
items: [
|
||||
items.oneOffWords({
|
||||
includedUsage: 0,
|
||||
billingUnits: 100,
|
||||
price: 25,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const { autumnV1 } = await initScenario({
|
||||
customerId,
|
||||
setup: [
|
||||
s.customer({ paymentMethod: "success" }),
|
||||
s.products({ list: [premium, pro, topupAddon] }),
|
||||
],
|
||||
actions: [s.billing.attach({ productId: premium.id })],
|
||||
});
|
||||
|
||||
// Apply a 20% discount to the subscription (matches production scenario)
|
||||
const { stripeCli, subscription: subBefore } = await getStripeSubscription({
|
||||
customerId,
|
||||
});
|
||||
|
||||
const coupon = await createPercentCoupon({
|
||||
stripeCli,
|
||||
percentOff: 20,
|
||||
});
|
||||
|
||||
await applySubscriptionDiscount({
|
||||
stripeCli,
|
||||
subscriptionId: subBefore.id,
|
||||
couponIds: [coupon.id],
|
||||
});
|
||||
|
||||
// Schedule downgrade: Premium -> Pro (creates a subscription schedule)
|
||||
await autumnV1.billing.attach({
|
||||
customer_id: customerId,
|
||||
product_id: pro.id,
|
||||
redirect_mode: "if_required",
|
||||
});
|
||||
|
||||
// Verify the downgrade was scheduled correctly
|
||||
const customer = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||
await expectProductCanceling({ customer, productId: premium.id });
|
||||
await expectProductScheduled({ customer, productId: pro.id });
|
||||
|
||||
await autumnV1.billing.attach({
|
||||
customer_id: customerId,
|
||||
product_id: topupAddon.id,
|
||||
redirect_mode: "if_required",
|
||||
options: [{ feature_id: TestFeature.Words, quantity: 100 }],
|
||||
});
|
||||
|
||||
const customerAfter = await autumnV1.customers.get<ApiCustomerV3>(customerId);
|
||||
expectCustomerFeatureCorrect({
|
||||
customer: customerAfter,
|
||||
featureId: TestFeature.Words,
|
||||
balance: 100,
|
||||
usage: 0,
|
||||
});
|
||||
|
||||
await expectCustomerInvoiceCorrect({
|
||||
customer: customerAfter,
|
||||
count: 2,
|
||||
latestTotal: 25,
|
||||
});
|
||||
});
|
||||
@@ -21,10 +21,15 @@ export const useEntity = () => {
|
||||
const { customer } = useCusQuery();
|
||||
const entities = (customer as FullCustomer)?.entities || [];
|
||||
|
||||
// #region agent log
|
||||
fetch('http://127.0.0.1:7322/ingest/302190cd-01f7-494e-9c36-a1625f5cf969',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'0967f4'},body:JSON.stringify({sessionId:'0967f4',location:'useSubscriptionStore.ts:useEntity',message:'entities debug',data:{customerExists:!!customer,entitiesRaw:(customer as any)?.entities,entitiesIsArray:Array.isArray((customer as any)?.entities),entitiesLength:entities.length,entitiesNullCount:entities.filter((e:any) => e == null).length,firstFewEntities:entities.slice(0,3).map((e:any) => e == null ? 'NULL' : {id:e?.id,internal_id:e?.internal_id}),selectedEntityId},timestamp:Date.now()})}).catch(()=>{});
|
||||
// #endregion
|
||||
|
||||
// Find the full entity object
|
||||
const entity = entities.find(
|
||||
(e: Entity) =>
|
||||
e.id === selectedEntityId || e.internal_id === selectedEntityId,
|
||||
e != null &&
|
||||
(e.id === selectedEntityId || e.internal_id === selectedEntityId),
|
||||
);
|
||||
|
||||
// Sync query param to store on mount/change
|
||||
|
||||
@@ -3,13 +3,17 @@ import { Button } from "@/components/v2/buttons/Button";
|
||||
import { CustomerBlockDialog } from "./CustomerBlockDialog";
|
||||
import { EdgeConfigDialog } from "./EdgeConfigDialog";
|
||||
import { FeatureFlagsDialog } from "./FeatureFlagsDialog";
|
||||
import { OrgLimitsDialog } from "./OrgLimitsDialog";
|
||||
import { RawEdgeConfigDialog } from "./RawEdgeConfigDialog";
|
||||
import { StripeSyncDialog } from "./StripeSyncDialog";
|
||||
|
||||
export function EdgeConfigTab() {
|
||||
const [requestBlockEditOpen, setRequestBlockEditOpen] = useState(false);
|
||||
const [requestBlockRawOpen, setRequestBlockRawOpen] = useState(false);
|
||||
const [featureFlagsOpen, setFeatureFlagsOpen] = useState(false);
|
||||
const [customerBlockOpen, setCustomerBlockOpen] = useState(false);
|
||||
const [orgLimitsOpen, setOrgLimitsOpen] = useState(false);
|
||||
const [stripeSyncOpen, setStripeSyncOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -71,6 +75,39 @@ export function EdgeConfigTab() {
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-border p-4 last:border-b-0">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="text-sm font-medium text-t1">Org Limits</div>
|
||||
<div className="text-xs text-t3">
|
||||
Per-org overrides for max customer products returned in queries
|
||||
(default 15).
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => setOrgLimitsOpen(true)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-border p-4 last:border-b-0">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="text-sm font-medium text-t1">Stripe Sync</div>
|
||||
<div className="text-xs text-t3">
|
||||
Enable Stripe webhook event syncing to the sync DB per org.
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
onClick={() => setStripeSyncOpen(true)}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FeatureFlagsDialog
|
||||
@@ -95,6 +132,13 @@ export function EdgeConfigTab() {
|
||||
onOpenChange={setRequestBlockRawOpen}
|
||||
configId="request-block"
|
||||
/>
|
||||
|
||||
<OrgLimitsDialog open={orgLimitsOpen} onOpenChange={setOrgLimitsOpen} />
|
||||
|
||||
<StripeSyncDialog
|
||||
open={stripeSyncOpen}
|
||||
onOpenChange={setStripeSyncOpen}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
342
vite/src/views/admin/components/OrgLimitsDialog.tsx
Normal file
342
vite/src/views/admin/components/OrgLimitsDialog.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/components/v2/badges/Badge";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/v2/dialogs/Dialog";
|
||||
import { Input } from "@/components/v2/inputs/Input";
|
||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { getBackendErr } from "@/utils/genUtils";
|
||||
|
||||
const DEFAULT_CUS_PRODUCT_LIMIT = 15;
|
||||
|
||||
type OrgLimitsEntry = {
|
||||
maxCusProducts?: number;
|
||||
};
|
||||
|
||||
type OrgLimitsConfig = {
|
||||
orgs: Record<string, OrgLimitsEntry>;
|
||||
configHealthy: boolean;
|
||||
configConfigured: boolean;
|
||||
lastSuccessAt: string | null;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG: OrgLimitsConfig = {
|
||||
orgs: {},
|
||||
configHealthy: false,
|
||||
configConfigured: false,
|
||||
lastSuccessAt: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const getEditableConfig = ({ config }: { config: OrgLimitsConfig }) => ({
|
||||
orgs: config.orgs,
|
||||
});
|
||||
|
||||
const getEntryRows = ({ config }: { config: OrgLimitsConfig }) => {
|
||||
return Object.entries(config.orgs)
|
||||
.map(([orgId, entry]) => ({
|
||||
orgId,
|
||||
maxCusProducts: entry.maxCusProducts ?? DEFAULT_CUS_PRODUCT_LIMIT,
|
||||
}))
|
||||
.sort((a, b) => a.orgId.localeCompare(b.orgId));
|
||||
};
|
||||
|
||||
export function OrgLimitsDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
const axiosInstance = useAxiosInstance();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [config, setConfig] = useState<OrgLimitsConfig>(DEFAULT_CONFIG);
|
||||
const [jsonText, setJsonText] = useState("");
|
||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||
const [syncSource, setSyncSource] = useState<"form" | "json">("form");
|
||||
const [newOrgId, setNewOrgId] = useState("");
|
||||
const [newLimit, setNewLimit] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
|
||||
void axiosInstance
|
||||
.get<OrgLimitsConfig>("/admin/org-limits-config")
|
||||
.then(({ data }) => {
|
||||
if (cancelled) return;
|
||||
const mergedConfig: OrgLimitsConfig = {
|
||||
...DEFAULT_CONFIG,
|
||||
...data,
|
||||
};
|
||||
setConfig(mergedConfig);
|
||||
setJsonText(
|
||||
JSON.stringify(getEditableConfig({ config: mergedConfig }), null, 2),
|
||||
);
|
||||
setJsonError(null);
|
||||
setSyncSource("form");
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled) {
|
||||
toast.error(getBackendErr(error, "Failed to load org limits config"));
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [axiosInstance, open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (syncSource !== "form") return;
|
||||
setJsonText(JSON.stringify(getEditableConfig({ config }), null, 2));
|
||||
setJsonError(null);
|
||||
}, [config, syncSource]);
|
||||
|
||||
const entryRows = useMemo(() => getEntryRows({ config }), [config]);
|
||||
|
||||
const handleJsonChange = (value: string | undefined) => {
|
||||
const text = value ?? "";
|
||||
setJsonText(text);
|
||||
setSyncSource("json");
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(text) as {
|
||||
orgs?: Record<string, OrgLimitsEntry>;
|
||||
};
|
||||
setConfig((current) => ({
|
||||
...current,
|
||||
orgs: parsed.orgs ?? {},
|
||||
}));
|
||||
setJsonError(null);
|
||||
} catch {
|
||||
setJsonError("Invalid JSON");
|
||||
}
|
||||
};
|
||||
|
||||
const addEntry = () => {
|
||||
const orgId = newOrgId.trim();
|
||||
const limit = parseInt(newLimit.trim(), 10);
|
||||
|
||||
if (!orgId || Number.isNaN(limit) || limit < 1) return;
|
||||
|
||||
setSyncSource("form");
|
||||
setConfig((current) => ({
|
||||
...current,
|
||||
orgs: {
|
||||
...current.orgs,
|
||||
[orgId]: { maxCusProducts: limit },
|
||||
},
|
||||
}));
|
||||
setNewOrgId("");
|
||||
setNewLimit("");
|
||||
};
|
||||
|
||||
const removeEntry = ({ orgId }: { orgId: string }) => {
|
||||
setSyncSource("form");
|
||||
setConfig((current) => {
|
||||
const nextOrgs = { ...current.orgs };
|
||||
delete nextOrgs[orgId];
|
||||
return { ...current, orgs: nextOrgs };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (jsonError) {
|
||||
toast.error("Fix JSON errors before saving");
|
||||
return;
|
||||
}
|
||||
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = JSON.parse(jsonText);
|
||||
} catch {
|
||||
toast.error("Invalid JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await axiosInstance.put("/admin/org-limits-config", payload);
|
||||
toast.success("Org limits config saved");
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
toast.error(getBackendErr(error, "Failed to save org limits config"));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-5xl bg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Org Limits</DialogTitle>
|
||||
<DialogDescription>
|
||||
Configure per-org limits such as max customer products returned in
|
||||
queries. Default is {DEFAULT_CUS_PRODUCT_LIMIT}.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{loading ? (
|
||||
<div className="py-8 text-center text-sm text-t3">Loading...</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-[320px_1fr] gap-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-xs font-medium uppercase tracking-wide text-t3">
|
||||
Org Overrides
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border p-3">
|
||||
<div className="mb-3 flex flex-col gap-2">
|
||||
<Input
|
||||
placeholder="Org ID or slug"
|
||||
value={newOrgId}
|
||||
onChange={(event) => setNewOrgId(event.target.value)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Max cusProducts (e.g. 30)"
|
||||
type="number"
|
||||
min={1}
|
||||
value={newLimit}
|
||||
onChange={(event) => setNewLimit(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={addEntry}
|
||||
disabled={
|
||||
!newOrgId.trim() ||
|
||||
!newLimit.trim() ||
|
||||
Number.isNaN(parseInt(newLimit, 10)) ||
|
||||
parseInt(newLimit, 10) < 1
|
||||
}
|
||||
>
|
||||
Add org limit
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||
{entryRows.length === 0 ? (
|
||||
<div className="text-xs italic text-t3">
|
||||
No org overrides — all orgs use default (
|
||||
{DEFAULT_CUS_PRODUCT_LIMIT})
|
||||
</div>
|
||||
) : (
|
||||
entryRows.map((entry) => (
|
||||
<div
|
||||
key={entry.orgId}
|
||||
className="flex items-start justify-between gap-3 rounded-lg border border-border p-2"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate font-mono text-xs text-t1">
|
||||
{entry.orgId}
|
||||
</div>
|
||||
<div className="text-xs text-t2">
|
||||
maxCusProducts: {entry.maxCusProducts}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => removeEntry({ orgId: entry.orgId })}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border p-3 text-xs text-t3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Badge
|
||||
variant="muted"
|
||||
className={
|
||||
config.configHealthy
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-700"
|
||||
: "border-amber-200 bg-amber-50 text-amber-700"
|
||||
}
|
||||
>
|
||||
{config.configHealthy
|
||||
? "Config healthy"
|
||||
: "Config unavailable"}
|
||||
</Badge>
|
||||
{config.lastSuccessAt && (
|
||||
<span>
|
||||
Last refresh:{" "}
|
||||
{new Date(config.lastSuccessAt).toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{config.configConfigured === false
|
||||
? "S3 org limits config is not configured."
|
||||
: config.error ||
|
||||
"Limits update within 5 minutes of saving."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-xs font-medium uppercase tracking-wide text-t3">
|
||||
Raw JSON
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-md border border-border">
|
||||
<Editor
|
||||
height="420px"
|
||||
language="json"
|
||||
value={jsonText}
|
||||
onChange={handleJsonChange}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 12,
|
||||
tabSize: 2,
|
||||
wordWrap: "on",
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
}}
|
||||
theme="vs-dark"
|
||||
/>
|
||||
</div>
|
||||
{jsonError && (
|
||||
<div className="text-xs text-red-500">{jsonError}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
isLoading={saving}
|
||||
disabled={loading || !!jsonError}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
306
vite/src/views/admin/components/StripeSyncDialog.tsx
Normal file
306
vite/src/views/admin/components/StripeSyncDialog.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Badge } from "@/components/v2/badges/Badge";
|
||||
import { Button } from "@/components/v2/buttons/Button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/v2/dialogs/Dialog";
|
||||
import { Input } from "@/components/v2/inputs/Input";
|
||||
import { useAxiosInstance } from "@/services/useAxiosInstance";
|
||||
import { getBackendErr } from "@/utils/genUtils";
|
||||
|
||||
type StripeSyncConfig = {
|
||||
enabledOrgIds: string[];
|
||||
configHealthy: boolean;
|
||||
configConfigured: boolean;
|
||||
lastSuccessAt: string | null;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
const DEFAULT_CONFIG: StripeSyncConfig = {
|
||||
enabledOrgIds: [],
|
||||
configHealthy: false,
|
||||
configConfigured: false,
|
||||
lastSuccessAt: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
export function StripeSyncDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
}: {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}) {
|
||||
const axiosInstance = useAxiosInstance();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [config, setConfig] = useState<StripeSyncConfig>(DEFAULT_CONFIG);
|
||||
const [jsonText, setJsonText] = useState("");
|
||||
const [jsonError, setJsonError] = useState<string | null>(null);
|
||||
const [syncSource, setSyncSource] = useState<"form" | "json">("form");
|
||||
const [newOrgId, setNewOrgId] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
|
||||
void axiosInstance
|
||||
.get<StripeSyncConfig>("/admin/stripe-sync-config")
|
||||
.then(({ data }) => {
|
||||
if (cancelled) return;
|
||||
const merged = { ...DEFAULT_CONFIG, ...data };
|
||||
setConfig(merged);
|
||||
setJsonText(
|
||||
JSON.stringify({ enabledOrgIds: merged.enabledOrgIds }, null, 2),
|
||||
);
|
||||
setJsonError(null);
|
||||
setSyncSource("form");
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled) {
|
||||
toast.error(
|
||||
getBackendErr(error, "Failed to load stripe sync config"),
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setLoading(false);
|
||||
});
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [axiosInstance, open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (syncSource !== "form") return;
|
||||
setJsonText(
|
||||
JSON.stringify({ enabledOrgIds: config.enabledOrgIds }, null, 2),
|
||||
);
|
||||
setJsonError(null);
|
||||
}, [config, syncSource]);
|
||||
|
||||
const sortedOrgIds = useMemo(
|
||||
() => [...config.enabledOrgIds].sort(),
|
||||
[config.enabledOrgIds],
|
||||
);
|
||||
|
||||
const handleJsonChange = (value: string | undefined) => {
|
||||
const text = value ?? "";
|
||||
setJsonText(text);
|
||||
setSyncSource("json");
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(text) as { enabledOrgIds?: string[] };
|
||||
setConfig((current) => ({
|
||||
...current,
|
||||
enabledOrgIds: parsed.enabledOrgIds ?? [],
|
||||
}));
|
||||
setJsonError(null);
|
||||
} catch {
|
||||
setJsonError("Invalid JSON");
|
||||
}
|
||||
};
|
||||
|
||||
const addOrg = () => {
|
||||
const orgId = newOrgId.trim();
|
||||
if (!orgId) return;
|
||||
if (config.enabledOrgIds.includes(orgId)) {
|
||||
toast.error("Org already in list");
|
||||
return;
|
||||
}
|
||||
|
||||
setSyncSource("form");
|
||||
setConfig((current) => ({
|
||||
...current,
|
||||
enabledOrgIds: [...current.enabledOrgIds, orgId],
|
||||
}));
|
||||
setNewOrgId("");
|
||||
};
|
||||
|
||||
const removeOrg = ({ orgId }: { orgId: string }) => {
|
||||
setSyncSource("form");
|
||||
setConfig((current) => ({
|
||||
...current,
|
||||
enabledOrgIds: current.enabledOrgIds.filter((id) => id !== orgId),
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (jsonError) {
|
||||
toast.error("Fix JSON errors before saving");
|
||||
return;
|
||||
}
|
||||
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = JSON.parse(jsonText);
|
||||
} catch {
|
||||
toast.error("Invalid JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await axiosInstance.put("/admin/stripe-sync-config", payload);
|
||||
toast.success("Stripe sync config saved");
|
||||
onOpenChange(false);
|
||||
} catch (error) {
|
||||
toast.error(getBackendErr(error, "Failed to save stripe sync config"));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-5xl bg-card">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Stripe Sync</DialogTitle>
|
||||
<DialogDescription>
|
||||
Manage which orgs have Stripe webhook events synced to the sync DB.
|
||||
Changes take effect within 60 seconds.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
{loading ? (
|
||||
<div className="py-8 text-center text-sm text-t3">Loading...</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-[320px_1fr] gap-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-xs font-medium uppercase tracking-wide text-t3">
|
||||
Enabled Orgs
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border p-3">
|
||||
<div className="mb-3 flex gap-2">
|
||||
<Input
|
||||
placeholder="Org ID or slug"
|
||||
value={newOrgId}
|
||||
onChange={(event) => setNewOrgId(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") addOrg();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={addOrg}
|
||||
disabled={!newOrgId.trim()}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 border-t border-border pt-3">
|
||||
{sortedOrgIds.length === 0 ? (
|
||||
<div className="text-xs italic text-t3">
|
||||
No orgs enabled — sync is disabled for all orgs.
|
||||
</div>
|
||||
) : (
|
||||
sortedOrgIds.map((orgId) => (
|
||||
<div
|
||||
key={orgId}
|
||||
className="flex items-center justify-between gap-3 rounded-lg border border-border p-2"
|
||||
>
|
||||
<div className="truncate font-mono text-xs text-t1">
|
||||
{orgId}
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => removeOrg({ orgId })}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-border p-3 text-xs text-t3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Badge
|
||||
variant="muted"
|
||||
className={
|
||||
config.configHealthy
|
||||
? "border-emerald-200 bg-emerald-50 text-emerald-700"
|
||||
: "border-amber-200 bg-amber-50 text-amber-700"
|
||||
}
|
||||
>
|
||||
{config.configHealthy
|
||||
? "Config healthy"
|
||||
: "Config unavailable"}
|
||||
</Badge>
|
||||
{config.lastSuccessAt && (
|
||||
<span>
|
||||
Last refresh:{" "}
|
||||
{new Date(config.lastSuccessAt).toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{config.configConfigured === false
|
||||
? "S3 stripe sync config is not configured."
|
||||
: config.error ||
|
||||
"Config updates within 60 seconds of saving."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="text-xs font-medium uppercase tracking-wide text-t3">
|
||||
Raw JSON
|
||||
</div>
|
||||
<div className="overflow-hidden rounded-md border border-border">
|
||||
<Editor
|
||||
height="420px"
|
||||
language="json"
|
||||
value={jsonText}
|
||||
onChange={handleJsonChange}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
fontSize: 12,
|
||||
tabSize: 2,
|
||||
wordWrap: "on",
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
}}
|
||||
theme="vs-dark"
|
||||
/>
|
||||
</div>
|
||||
{jsonError && (
|
||||
<div className="text-xs text-red-500">{jsonError}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
isLoading={saving}
|
||||
disabled={loading || !!jsonError}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -9,13 +9,13 @@ export const OrgLogo = ({ org }: { org: FrontendOrg }) => {
|
||||
"rounded-md overflow-hidden flex items-center justify-center scale-100 translate-x-px bg-zinc-200 w-5 h-5 min-w-5 min-h-5",
|
||||
)}
|
||||
>
|
||||
{org.logo ? (
|
||||
{/* {org.logo ? (
|
||||
<img src={org.logo} alt={org.name} className="w-full h-full" />
|
||||
) : (
|
||||
<span className="w-5 h-5 flex items-center justify-center bg-linear-to-r from-purple-600 via-purple-500 to-purple-gradient text-white text-xs">
|
||||
{firstLetter}
|
||||
</span>
|
||||
)}
|
||||
) : ( */}
|
||||
<span className="w-5 h-5 flex items-center justify-center bg-linear-to-r from-purple-600 via-purple-500 to-purple-gradient text-white text-xs">
|
||||
{firstLetter}
|
||||
</span>
|
||||
{/* )} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -194,7 +194,10 @@ export const EditPlanHeader = () => {
|
||||
{hasRCMapping && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<IconBadge variant="muted" icon={<RevenueCatIcon size={14} />}>
|
||||
<IconBadge
|
||||
variant="muted"
|
||||
icon={<RevenueCatIcon size={14} />}
|
||||
>
|
||||
RC
|
||||
</IconBadge>
|
||||
</TooltipTrigger>
|
||||
@@ -209,8 +212,7 @@ export const EditPlanHeader = () => {
|
||||
<IconBadge
|
||||
variant="muted"
|
||||
icon={<TriangleIcon size={12} weight="fill" />}
|
||||
>
|
||||
</IconBadge>
|
||||
></IconBadge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
This plan is linked to Vercel Marketplace
|
||||
|
||||
Reference in New Issue
Block a user