Compare commits
5 Commits
cloudflare
...
dev/fix-we
Author | SHA1 | Date | |
---|---|---|---|
|
5136f64252 | ||
|
b8d3d09d9e | ||
|
5cebf2ef00 | ||
|
787548a269 | ||
|
aea55ee4bb |
@ -35,6 +35,14 @@ After that, you can return to the blog post and you can read the rest of the blo
|
|||||||
|
|
||||||
For more details, please check out the documentation: [Blog](https://mksaas.com/docs/blog).
|
For more details, please check out the documentation: [Blog](https://mksaas.com/docs/blog).
|
||||||
|
|
||||||
|
Test show Tweet in the blog post.
|
||||||
|
|
||||||
|
<XEmbedClient url="https://x.com/mksaascom/status/1960417768505008291" width={500} />
|
||||||
|
|
||||||
|
Test show YouTube video in the blog post.
|
||||||
|
|
||||||
|
<YoutubeVideo url="https://www.youtube.com/embed/xvoeSnlFZJk" width={500} />
|
||||||
|
|
||||||
Now the rest of the blog post is premium content.
|
Now the rest of the blog post is premium content.
|
||||||
|
|
||||||
<PremiumContent>
|
<PremiumContent>
|
||||||
|
@ -35,6 +35,14 @@ CVV: 567
|
|||||||
|
|
||||||
更多详情,请参考文档:[博客](https://mksaas.com/docs/blog)。
|
更多详情,请参考文档:[博客](https://mksaas.com/docs/blog)。
|
||||||
|
|
||||||
|
测试展示 X 帖子。
|
||||||
|
|
||||||
|
<XEmbedClient url="https://x.com/mksaascom/status/1960417768505008291" width={500} />
|
||||||
|
|
||||||
|
测试展示 YouTube 视频。
|
||||||
|
|
||||||
|
<YoutubeVideo url="https://www.youtube.com/embed/xvoeSnlFZJk" width={500} />
|
||||||
|
|
||||||
现在剩下的内容是付费内容。
|
现在剩下的内容是付费内容。
|
||||||
|
|
||||||
<PremiumContent>
|
<PremiumContent>
|
||||||
|
@ -120,6 +120,7 @@
|
|||||||
"react-hook-form": "^7.62.0",
|
"react-hook-form": "^7.62.0",
|
||||||
"react-remove-scroll": "^2.6.3",
|
"react-remove-scroll": "^2.6.3",
|
||||||
"react-resizable-panels": "^2.1.7",
|
"react-resizable-panels": "^2.1.7",
|
||||||
|
"react-social-media-embed": "^2.5.18",
|
||||||
"react-syntax-highlighter": "^15.6.3",
|
"react-syntax-highlighter": "^15.6.3",
|
||||||
"react-tweet": "^3.2.2",
|
"react-tweet": "^3.2.2",
|
||||||
"react-use-measure": "^2.1.7",
|
"react-use-measure": "^2.1.7",
|
||||||
|
125
pnpm-lock.yaml
generated
125
pnpm-lock.yaml
generated
@ -293,6 +293,9 @@ importers:
|
|||||||
react-resizable-panels:
|
react-resizable-panels:
|
||||||
specifier: ^2.1.7
|
specifier: ^2.1.7
|
||||||
version: 2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
react-social-media-embed:
|
||||||
|
specifier: ^2.5.18
|
||||||
|
version: 2.5.18(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
react-syntax-highlighter:
|
react-syntax-highlighter:
|
||||||
specifier: ^15.6.3
|
specifier: ^15.6.3
|
||||||
version: 15.6.3(react@19.0.0)
|
version: 15.6.3(react@19.0.0)
|
||||||
@ -3676,6 +3679,9 @@ packages:
|
|||||||
'@types/unist@3.0.3':
|
'@types/unist@3.0.3':
|
||||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||||
|
|
||||||
|
'@types/youtube-player@5.5.11':
|
||||||
|
resolution: {integrity: sha512-pM41CDBqJqBmTeJWnF7NOGz82IQoYOhqzMYXv5vKCXBqGiYSLldxMtpCk6KAEtADTy49S45AriYaCaZyeUX38Q==}
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.40.0':
|
'@typescript-eslint/project-service@8.40.0':
|
||||||
resolution: {integrity: sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==}
|
resolution: {integrity: sha512-/A89vz7Wf5DEXsGVvcGdYKbVM9F7DyFXj52lNYUDS1L9yJfqjW/fIp5PgMuEJL/KeqVTe2QSbXAGUZljDUpArw==}
|
||||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||||
@ -3939,6 +3945,9 @@ packages:
|
|||||||
class-variance-authority@0.7.1:
|
class-variance-authority@0.7.1:
|
||||||
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
|
||||||
|
|
||||||
|
classnames@2.5.1:
|
||||||
|
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||||
|
|
||||||
cli-cursor@3.1.0:
|
cli-cursor@3.1.0:
|
||||||
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@ -4086,6 +4095,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==}
|
resolution: {integrity: sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
debug@2.6.9:
|
||||||
|
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
|
||||||
debug@4.3.7:
|
debug@4.3.7:
|
||||||
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
|
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
|
||||||
engines: {node: '>=6.0'}
|
engines: {node: '>=6.0'}
|
||||||
@ -4983,6 +5000,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==}
|
resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
|
|
||||||
|
load-script@1.0.0:
|
||||||
|
resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==}
|
||||||
|
|
||||||
locate-path@6.0.0:
|
locate-path@6.0.0:
|
||||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -5269,6 +5289,9 @@ packages:
|
|||||||
react-dom:
|
react-dom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
ms@2.0.0:
|
||||||
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
|
|
||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
@ -5698,6 +5721,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17 || ^18 || ^19
|
react: ^16.8.0 || ^17 || ^18 || ^19
|
||||||
|
|
||||||
|
react-html-props@2.1.1:
|
||||||
|
resolution: {integrity: sha512-tM+YCYlr90m3JontKUAa+gNVU2zkyprlCS7OQ9aa3z2MfyJjAioJzrSmi1Vef/+UCTE6CQlPqLX4ebdLIJDKxw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0
|
||||||
|
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0
|
||||||
|
|
||||||
react-is@16.13.1:
|
react-is@16.13.1:
|
||||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||||
|
|
||||||
@ -5761,6 +5790,12 @@ packages:
|
|||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
react-social-media-embed@2.5.18:
|
||||||
|
resolution: {integrity: sha512-+PkzLRGAwnySkxKajaiK5VD+EjOhlFsh/vjNxgHsDfKBTseDpFxPrMXXQWkk6BRCwFBNVWX+V1HZ9AU0y54Wgw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0
|
||||||
|
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0
|
||||||
|
|
||||||
react-style-singleton@2.2.3:
|
react-style-singleton@2.2.3:
|
||||||
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
|
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -5771,6 +5806,12 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-sub-unsub@2.2.8:
|
||||||
|
resolution: {integrity: sha512-o3tmiOOZPdQUCmRhkdCHXRFLOHnCwdz/N3QZ1JQ14fQGA2HysKMF0kWu56ERnQUCK7wYVCQzI8pFbnivAYNQ+A==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0
|
||||||
|
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 || ^22.0.0
|
||||||
|
|
||||||
react-syntax-highlighter@15.6.3:
|
react-syntax-highlighter@15.6.3:
|
||||||
resolution: {integrity: sha512-HebdyA9r20hgmA0q8RyRJ4c/vB4E6KL2HeWb5MNjU3iJEiT2w9jfU2RJsmI6f3Cy3SGE5tm0AIkBzM/E7e9/lQ==}
|
resolution: {integrity: sha512-HebdyA9r20hgmA0q8RyRJ4c/vB4E6KL2HeWb5MNjU3iJEiT2w9jfU2RJsmI6f3Cy3SGE5tm0AIkBzM/E7e9/lQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -5788,6 +5829,13 @@ packages:
|
|||||||
react: ^18.0.0 || ^19.0.0
|
react: ^18.0.0 || ^19.0.0
|
||||||
react-dom: ^18.0.0 || ^19.0.0
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
|
react-twitter-embed@4.0.4:
|
||||||
|
resolution: {integrity: sha512-2JIL7qF+U62zRzpsh6SZDXNI3hRNVYf5vOZ1WRcMvwKouw+xC00PuFaD0aEp2wlyGaZ+f4x2VvX+uDadFQ3HVA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
|
||||||
|
|
||||||
react-use-measure@2.1.7:
|
react-use-measure@2.1.7:
|
||||||
resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
|
resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -5797,6 +5845,12 @@ packages:
|
|||||||
react-dom:
|
react-dom:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-youtube@10.1.0:
|
||||||
|
resolution: {integrity: sha512-ZfGtcVpk0SSZtWCSTYOQKhfx5/1cfyEW1JN/mugGNfAxT3rmVJeMbGpA9+e78yG21ls5nc/5uZJETE3cm3knBg==}
|
||||||
|
engines: {node: '>= 14.x'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=0.14.1'
|
||||||
|
|
||||||
react@19.0.0:
|
react@19.0.0:
|
||||||
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
|
resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -5908,6 +5962,9 @@ packages:
|
|||||||
scheduler@0.25.0:
|
scheduler@0.25.0:
|
||||||
resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
|
resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==}
|
||||||
|
|
||||||
|
scriptjs@2.5.9:
|
||||||
|
resolution: {integrity: sha512-qGVDoreyYiP1pkQnbnFAUIS5AjenNwwQBdl7zeos9etl+hYKWahjRTfzAZZYBv5xNHx7vNKCmaLDQZ6Fr2AEXg==}
|
||||||
|
|
||||||
scroll-into-view-if-needed@3.1.0:
|
scroll-into-view-if-needed@3.1.0:
|
||||||
resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
|
resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
|
||||||
|
|
||||||
@ -5970,6 +6027,9 @@ packages:
|
|||||||
simple-swizzle@0.2.2:
|
simple-swizzle@0.2.2:
|
||||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||||
|
|
||||||
|
sister@3.0.2:
|
||||||
|
resolution: {integrity: sha512-p19rtTs+NksBRKW9qn0UhZ8/TUI9BPw9lmtHny+Y3TinWlOa9jWh9xB0AtPSdmOy49NJJJSSe0Ey4C7h0TrcYA==}
|
||||||
|
|
||||||
smol-toml@1.3.4:
|
smol-toml@1.3.4:
|
||||||
resolution: {integrity: sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA==}
|
resolution: {integrity: sha512-UOPtVuYkzYGee0Bd2Szz8d2G3RfMfJ2t3qVdZUAozZyAk+a0Sxa+QKix0YCwjL/A1RR0ar44nCxaoN9FxdJGwA==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
@ -6338,6 +6398,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
youtube-player@5.5.2:
|
||||||
|
resolution: {integrity: sha512-ZGtsemSpXnDky2AUYWgxjaopgB+shFHgXVpiJFeNB5nWEugpW1KWYDaHKuLqh2b67r24GtP6HoSW5swvf0fFIQ==}
|
||||||
|
|
||||||
zod-to-json-schema@3.24.2:
|
zod-to-json-schema@3.24.2:
|
||||||
resolution: {integrity: sha512-pNUqrcSxuuB3/+jBbU8qKUbTbDqYUaG1vf5cXFjbhGgoUuA1amO/y4Q8lzfOhHU8HNPK6VFJ18lBDKj3OHyDsg==}
|
resolution: {integrity: sha512-pNUqrcSxuuB3/+jBbU8qKUbTbDqYUaG1vf5cXFjbhGgoUuA1amO/y4Q8lzfOhHU8HNPK6VFJ18lBDKj3OHyDsg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -9420,6 +9483,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/unist@3.0.3': {}
|
'@types/unist@3.0.3': {}
|
||||||
|
|
||||||
|
'@types/youtube-player@5.5.11': {}
|
||||||
|
|
||||||
'@typescript-eslint/project-service@8.40.0(typescript@5.8.3)':
|
'@typescript-eslint/project-service@8.40.0(typescript@5.8.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.40.0(typescript@5.8.3)
|
'@typescript-eslint/tsconfig-utils': 8.40.0(typescript@5.8.3)
|
||||||
@ -9675,6 +9740,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
|
|
||||||
|
classnames@2.5.1: {}
|
||||||
|
|
||||||
cli-cursor@3.1.0:
|
cli-cursor@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
restore-cursor: 3.1.0
|
restore-cursor: 3.1.0
|
||||||
@ -9800,6 +9867,10 @@ snapshots:
|
|||||||
|
|
||||||
debounce@2.0.0: {}
|
debounce@2.0.0: {}
|
||||||
|
|
||||||
|
debug@2.6.9:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.0.0
|
||||||
|
|
||||||
debug@4.3.7:
|
debug@4.3.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@ -10813,6 +10884,8 @@ snapshots:
|
|||||||
lightningcss-win32-arm64-msvc: 1.29.2
|
lightningcss-win32-arm64-msvc: 1.29.2
|
||||||
lightningcss-win32-x64-msvc: 1.29.2
|
lightningcss-win32-x64-msvc: 1.29.2
|
||||||
|
|
||||||
|
load-script@1.0.0: {}
|
||||||
|
|
||||||
locate-path@6.0.0:
|
locate-path@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
@ -11362,6 +11435,8 @@ snapshots:
|
|||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
react-dom: 19.0.0(react@19.0.0)
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
|
||||||
|
ms@2.0.0: {}
|
||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
nan@2.22.0:
|
nan@2.22.0:
|
||||||
@ -11859,6 +11934,11 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
|
|
||||||
|
react-html-props@2.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.0.0
|
||||||
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
|
||||||
react-is@16.13.1: {}
|
react-is@16.13.1: {}
|
||||||
|
|
||||||
react-is@18.3.1: {}
|
react-is@18.3.1: {}
|
||||||
@ -11933,6 +12013,19 @@ snapshots:
|
|||||||
react-dom: 19.0.0(react@19.0.0)
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
react-transition-group: 4.4.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
|
||||||
|
react-social-media-embed@2.5.18(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
|
dependencies:
|
||||||
|
'@types/youtube-player': 5.5.11
|
||||||
|
classnames: 2.5.1
|
||||||
|
react: 19.0.0
|
||||||
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
react-html-props: 2.1.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
react-sub-unsub: 2.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
react-twitter-embed: 4.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
react-youtube: 10.1.0(react@19.0.0)
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
react-style-singleton@2.2.3(@types/react@19.0.9)(react@19.0.0):
|
react-style-singleton@2.2.3(@types/react@19.0.9)(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
get-nonce: 1.0.1
|
get-nonce: 1.0.1
|
||||||
@ -11941,6 +12034,11 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.9
|
'@types/react': 19.0.9
|
||||||
|
|
||||||
|
react-sub-unsub@2.2.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.0.0
|
||||||
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
|
||||||
react-syntax-highlighter@15.6.3(react@19.0.0):
|
react-syntax-highlighter@15.6.3(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.27.6
|
'@babel/runtime': 7.27.6
|
||||||
@ -11968,12 +12066,27 @@ snapshots:
|
|||||||
react-dom: 19.0.0(react@19.0.0)
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
swr: 2.3.2(react@19.0.0)
|
swr: 2.3.2(react@19.0.0)
|
||||||
|
|
||||||
|
react-twitter-embed@4.0.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.0.0
|
||||||
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
scriptjs: 2.5.9
|
||||||
|
|
||||||
react-use-measure@2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
react-use-measure@2.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
react-dom: 19.0.0(react@19.0.0)
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
|
||||||
|
react-youtube@10.1.0(react@19.0.0):
|
||||||
|
dependencies:
|
||||||
|
fast-deep-equal: 3.1.3
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 19.0.0
|
||||||
|
youtube-player: 5.5.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
react@19.0.0: {}
|
react@19.0.0: {}
|
||||||
|
|
||||||
readable-stream@3.6.2:
|
readable-stream@3.6.2:
|
||||||
@ -12156,6 +12269,8 @@ snapshots:
|
|||||||
|
|
||||||
scheduler@0.25.0: {}
|
scheduler@0.25.0: {}
|
||||||
|
|
||||||
|
scriptjs@2.5.9: {}
|
||||||
|
|
||||||
scroll-into-view-if-needed@3.1.0:
|
scroll-into-view-if-needed@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
compute-scroll-into-view: 3.1.1
|
compute-scroll-into-view: 3.1.1
|
||||||
@ -12271,6 +12386,8 @@ snapshots:
|
|||||||
is-arrayish: 0.3.2
|
is-arrayish: 0.3.2
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
sister@3.0.2: {}
|
||||||
|
|
||||||
smol-toml@1.3.4: {}
|
smol-toml@1.3.4: {}
|
||||||
|
|
||||||
socket.io-adapter@2.5.5:
|
socket.io-adapter@2.5.5:
|
||||||
@ -12655,6 +12772,14 @@ snapshots:
|
|||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
youtube-player@5.5.2:
|
||||||
|
dependencies:
|
||||||
|
debug: 2.6.9
|
||||||
|
load-script: 1.0.0
|
||||||
|
sister: 3.0.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
zod-to-json-schema@3.24.2(zod@3.25.64):
|
zod-to-json-schema@3.24.2(zod@3.25.64):
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.64
|
zod: 3.25.64
|
||||||
|
@ -12,6 +12,7 @@ import defaultMdxComponents from 'fumadocs-ui/mdx';
|
|||||||
import * as LucideIcons from 'lucide-react';
|
import * as LucideIcons from 'lucide-react';
|
||||||
import type { MDXComponents } from 'mdx/types';
|
import type { MDXComponents } from 'mdx/types';
|
||||||
import type { ComponentProps, FC } from 'react';
|
import type { ComponentProps, FC } from 'react';
|
||||||
|
import { XEmbedClient } from './xembed';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enhanced MDX Content component that includes commonly used MDX components
|
* Enhanced MDX Content component that includes commonly used MDX components
|
||||||
@ -23,6 +24,7 @@ export function getMDXComponents(components?: MDXComponents): MDXComponents {
|
|||||||
...defaultMdxComponents,
|
...defaultMdxComponents,
|
||||||
...LucideIcons,
|
...LucideIcons,
|
||||||
// ...((await import('lucide-react')) as unknown as MDXComponents),
|
// ...((await import('lucide-react')) as unknown as MDXComponents),
|
||||||
|
XEmbedClient,
|
||||||
YoutubeVideo,
|
YoutubeVideo,
|
||||||
PremiumContent,
|
PremiumContent,
|
||||||
Tabs,
|
Tabs,
|
||||||
|
16
src/components/docs/xembed.tsx
Normal file
16
src/components/docs/xembed.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { XEmbed, type XEmbedProps } from 'react-social-media-embed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Embedding X Posts in Fumadocs
|
||||||
|
*
|
||||||
|
* https://rjv.im/blog/solution/embed-x-post-in-fuma-docs
|
||||||
|
*/
|
||||||
|
export function XEmbedClient({ ...props }: XEmbedProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<XEmbed {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
3
src/db/migrations/0004_clever_molly_hayes.sql
Normal file
3
src/db/migrations/0004_clever_molly_hayes.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "payment" ADD COLUMN "invoice_id" text;--> statement-breakpoint
|
||||||
|
CREATE INDEX "payment_invoice_id_idx" ON "payment" USING btree ("invoice_id");--> statement-breakpoint
|
||||||
|
ALTER TABLE "payment" ADD CONSTRAINT "payment_invoice_id_unique" UNIQUE("invoice_id");
|
946
src/db/migrations/meta/0004_snapshot.json
Normal file
946
src/db/migrations/meta/0004_snapshot.json
Normal file
@ -0,0 +1,946 @@
|
|||||||
|
{
|
||||||
|
"id": "adff59d1-8ceb-4472-ae47-148a950e700a",
|
||||||
|
"prevId": "318baf42-b0f6-4288-807b-778767149685",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.account": {
|
||||||
|
"name": "account",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"account_id": {
|
||||||
|
"name": "account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"provider_id": {
|
||||||
|
"name": "provider_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"access_token": {
|
||||||
|
"name": "access_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token": {
|
||||||
|
"name": "refresh_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"id_token": {
|
||||||
|
"name": "id_token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"access_token_expires_at": {
|
||||||
|
"name": "access_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"refresh_token_expires_at": {
|
||||||
|
"name": "refresh_token_expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"name": "scope",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"account_user_id_idx": {
|
||||||
|
"name": "account_user_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"account_account_id_idx": {
|
||||||
|
"name": "account_account_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "account_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"account_provider_id_idx": {
|
||||||
|
"name": "account_provider_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "provider_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"account_user_id_user_id_fk": {
|
||||||
|
"name": "account_user_id_user_id_fk",
|
||||||
|
"tableFrom": "account",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.credit_transaction": {
|
||||||
|
"name": "credit_transaction",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"amount": {
|
||||||
|
"name": "amount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"remaining_amount": {
|
||||||
|
"name": "remaining_amount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"payment_id": {
|
||||||
|
"name": "payment_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"expiration_date": {
|
||||||
|
"name": "expiration_date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"expiration_date_processed_at": {
|
||||||
|
"name": "expiration_date_processed_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"credit_transaction_user_id_idx": {
|
||||||
|
"name": "credit_transaction_user_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"credit_transaction_type_idx": {
|
||||||
|
"name": "credit_transaction_type_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "type",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"credit_transaction_user_id_user_id_fk": {
|
||||||
|
"name": "credit_transaction_user_id_user_id_fk",
|
||||||
|
"tableFrom": "credit_transaction",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.payment": {
|
||||||
|
"name": "payment",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"price_id": {
|
||||||
|
"name": "price_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"interval": {
|
||||||
|
"name": "interval",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"subscription_id": {
|
||||||
|
"name": "subscription_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"name": "session_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"invoice_id": {
|
||||||
|
"name": "invoice_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"period_start": {
|
||||||
|
"name": "period_start",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"period_end": {
|
||||||
|
"name": "period_end",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"cancel_at_period_end": {
|
||||||
|
"name": "cancel_at_period_end",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"trial_start": {
|
||||||
|
"name": "trial_start",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"trial_end": {
|
||||||
|
"name": "trial_end",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"payment_type_idx": {
|
||||||
|
"name": "payment_type_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "type",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"payment_price_id_idx": {
|
||||||
|
"name": "payment_price_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "price_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"payment_user_id_idx": {
|
||||||
|
"name": "payment_user_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"payment_customer_id_idx": {
|
||||||
|
"name": "payment_customer_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "customer_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"payment_status_idx": {
|
||||||
|
"name": "payment_status_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "status",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"payment_subscription_id_idx": {
|
||||||
|
"name": "payment_subscription_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "subscription_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"payment_session_id_idx": {
|
||||||
|
"name": "payment_session_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "session_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"payment_invoice_id_idx": {
|
||||||
|
"name": "payment_invoice_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "invoice_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"payment_user_id_user_id_fk": {
|
||||||
|
"name": "payment_user_id_user_id_fk",
|
||||||
|
"tableFrom": "payment",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"payment_invoice_id_unique": {
|
||||||
|
"name": "payment_invoice_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"invoice_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.session": {
|
||||||
|
"name": "session",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"token": {
|
||||||
|
"name": "token",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"impersonated_by": {
|
||||||
|
"name": "impersonated_by",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"session_token_idx": {
|
||||||
|
"name": "session_token_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "token",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"session_user_id_idx": {
|
||||||
|
"name": "session_user_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"session_user_id_user_id_fk": {
|
||||||
|
"name": "session_user_id_user_id_fk",
|
||||||
|
"tableFrom": "session",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"session_token_unique": {
|
||||||
|
"name": "session_token_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"token"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user": {
|
||||||
|
"name": "user",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_verified": {
|
||||||
|
"name": "email_verified",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"name": "image",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"banned": {
|
||||||
|
"name": "banned",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_reason": {
|
||||||
|
"name": "ban_reason",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ban_expires": {
|
||||||
|
"name": "ban_expires",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"customer_id": {
|
||||||
|
"name": "customer_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_id_idx": {
|
||||||
|
"name": "user_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"user_customer_id_idx": {
|
||||||
|
"name": "user_customer_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "customer_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
},
|
||||||
|
"user_role_idx": {
|
||||||
|
"name": "user_role_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "role",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"user_email_unique": {
|
||||||
|
"name": "user_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.user_credit": {
|
||||||
|
"name": "user_credit",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"current_credits": {
|
||||||
|
"name": "current_credits",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"last_refresh_at": {
|
||||||
|
"name": "last_refresh_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"user_credit_user_id_idx": {
|
||||||
|
"name": "user_credit_user_id_idx",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"expression": "user_id",
|
||||||
|
"isExpression": false,
|
||||||
|
"asc": true,
|
||||||
|
"nulls": "last"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"isUnique": false,
|
||||||
|
"concurrently": false,
|
||||||
|
"method": "btree",
|
||||||
|
"with": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_credit_user_id_user_id_fk": {
|
||||||
|
"name": "user_credit_user_id_user_id_fk",
|
||||||
|
"tableFrom": "user_credit",
|
||||||
|
"tableTo": "user",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.verification": {
|
||||||
|
"name": "verification",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"identifier": {
|
||||||
|
"name": "identifier",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"name": "value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"name": "expires_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
@ -29,6 +29,13 @@
|
|||||||
"when": 1752992749001,
|
"when": 1752992749001,
|
||||||
"tag": "0003_loving_risque",
|
"tag": "0003_loving_risque",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1757258758531,
|
||||||
|
"tag": "0004_clever_molly_hayes",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -72,6 +72,7 @@ export const payment = pgTable("payment", {
|
|||||||
customerId: text('customer_id').notNull(),
|
customerId: text('customer_id').notNull(),
|
||||||
subscriptionId: text('subscription_id'),
|
subscriptionId: text('subscription_id'),
|
||||||
sessionId: text('session_id'),
|
sessionId: text('session_id'),
|
||||||
|
invoiceId: text('invoice_id').unique(), // unique constraint for avoiding duplicate processing
|
||||||
status: text('status').notNull(),
|
status: text('status').notNull(),
|
||||||
periodStart: timestamp('period_start'),
|
periodStart: timestamp('period_start'),
|
||||||
periodEnd: timestamp('period_end'),
|
periodEnd: timestamp('period_end'),
|
||||||
@ -88,6 +89,7 @@ export const payment = pgTable("payment", {
|
|||||||
paymentStatusIdx: index("payment_status_idx").on(table.status),
|
paymentStatusIdx: index("payment_status_idx").on(table.status),
|
||||||
paymentSubscriptionIdIdx: index("payment_subscription_id_idx").on(table.subscriptionId),
|
paymentSubscriptionIdIdx: index("payment_subscription_id_idx").on(table.subscriptionId),
|
||||||
paymentSessionIdIdx: index("payment_session_id_idx").on(table.sessionId),
|
paymentSessionIdIdx: index("payment_session_id_idx").on(table.sessionId),
|
||||||
|
paymentInvoiceIdIdx: index("payment_invoice_id_idx").on(table.invoiceId),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const userCredit = pgTable("user_credit", {
|
export const userCredit = pgTable("user_credit", {
|
||||||
@ -108,7 +110,7 @@ export const creditTransaction = pgTable("credit_transaction", {
|
|||||||
description: text("description"),
|
description: text("description"),
|
||||||
amount: integer("amount").notNull(),
|
amount: integer("amount").notNull(),
|
||||||
remainingAmount: integer("remaining_amount"),
|
remainingAmount: integer("remaining_amount"),
|
||||||
paymentId: text("payment_id"),
|
paymentId: text("payment_id"), // payment id is actually invoice id
|
||||||
expirationDate: timestamp("expiration_date"),
|
expirationDate: timestamp("expiration_date"),
|
||||||
expirationDateProcessedAt: timestamp("expiration_date_processed_at"),
|
expirationDateProcessedAt: timestamp("expiration_date_processed_at"),
|
||||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||||
|
@ -9,11 +9,7 @@ import { getCreditPackageById } from '@/credits/server';
|
|||||||
import { CREDIT_TRANSACTION_TYPE } from '@/credits/types';
|
import { CREDIT_TRANSACTION_TYPE } from '@/credits/types';
|
||||||
import { getDb } from '@/db';
|
import { getDb } from '@/db';
|
||||||
import { payment, user } from '@/db/schema';
|
import { payment, user } from '@/db/schema';
|
||||||
import {
|
import { findPlanByPlanId, findPriceInPlan } from '@/lib/price-plan';
|
||||||
findPlanByPlanId,
|
|
||||||
findPlanByPriceId,
|
|
||||||
findPriceInPlan,
|
|
||||||
} from '@/lib/price-plan';
|
|
||||||
import { sendNotification } from '@/notification/notification';
|
import { sendNotification } from '@/notification/notification';
|
||||||
import { desc, eq } from 'drizzle-orm';
|
import { desc, eq } from 'drizzle-orm';
|
||||||
import { Stripe } from 'stripe';
|
import { Stripe } from 'stripe';
|
||||||
@ -492,6 +488,15 @@ export class StripeProvider implements PaymentProvider {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (eventType.startsWith('invoice.')) {
|
||||||
|
// Handle invoice events
|
||||||
|
switch (eventType) {
|
||||||
|
case 'invoice.payment_succeeded': {
|
||||||
|
const invoice = event.data.object as Stripe.Invoice;
|
||||||
|
await this.onInvoicePaymentSucceeded(invoice);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (eventType.startsWith('checkout.')) {
|
} else if (eventType.startsWith('checkout.')) {
|
||||||
// Handle checkout events
|
// Handle checkout events
|
||||||
if (eventType === 'checkout.session.completed') {
|
if (eventType === 'checkout.session.completed') {
|
||||||
@ -514,115 +519,426 @@ export class StripeProvider implements PaymentProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create payment record
|
* Handle successful invoice payment - NEW ARCHITECTURE
|
||||||
|
* Only create payment records here after payment is confirmed
|
||||||
|
*
|
||||||
|
* For one-time payments, the order of events may be:
|
||||||
|
* checkout.session.completed
|
||||||
|
* invoice.payment_succeeded
|
||||||
|
*
|
||||||
|
* For subscription payments, the order of events may be:
|
||||||
|
* checkout.session.completed
|
||||||
|
* customer.subscription.created
|
||||||
|
* customer.subscription.updated
|
||||||
|
* invoice.payment_succeeded
|
||||||
|
*
|
||||||
|
* @param invoice Stripe invoice
|
||||||
|
*/
|
||||||
|
private async onInvoicePaymentSucceeded(
|
||||||
|
invoice: Stripe.Invoice
|
||||||
|
): Promise<void> {
|
||||||
|
console.log('>> Handle invoice payment succeeded');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subscriptionId = invoice.subscription as string | null;
|
||||||
|
|
||||||
|
if (subscriptionId) {
|
||||||
|
// This is a subscription payment
|
||||||
|
await this.createSubscriptionPayment(invoice, subscriptionId);
|
||||||
|
} else {
|
||||||
|
// This is a one-time payment
|
||||||
|
await this.createOneTimePayment(invoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('<< Successfully processed invoice payment');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('<< Handle invoice payment succeeded error:', error);
|
||||||
|
|
||||||
|
// Check if it's a duplicate invoice error (database constraint violation)
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes('unique constraint')
|
||||||
|
) {
|
||||||
|
console.log('<< Invoice already processed:', invoice.id);
|
||||||
|
return; // Don't throw, this is expected for duplicate processing
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other errors, let Stripe retry
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create subscription payment record and process benefits - NEW ARCHITECTURE
|
||||||
|
*
|
||||||
|
* The order of events may be:
|
||||||
|
* checkout.session.completed
|
||||||
|
* customer.subscription.created
|
||||||
|
* customer.subscription.updated
|
||||||
|
* invoice.payment_succeeded
|
||||||
|
*
|
||||||
|
* @param invoice Stripe invoice
|
||||||
|
* @param subscriptionId Subscription ID
|
||||||
|
*/
|
||||||
|
private async createSubscriptionPayment(
|
||||||
|
invoice: Stripe.Invoice,
|
||||||
|
subscriptionId: string
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(
|
||||||
|
'>> Create subscription payment record for subscription:',
|
||||||
|
subscriptionId
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get subscription details from Stripe
|
||||||
|
const subscription =
|
||||||
|
await this.stripe.subscriptions.retrieve(subscriptionId);
|
||||||
|
const customerId = subscription.customer as string;
|
||||||
|
|
||||||
|
// Get priceId from subscription items
|
||||||
|
const priceId = subscription.items.data[0]?.price.id;
|
||||||
|
if (!priceId) {
|
||||||
|
console.warn('<< No priceId found for subscription');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get userId from subscription metadata or fallback to customerId lookup
|
||||||
|
let userId: string | undefined = subscription.metadata.userId;
|
||||||
|
|
||||||
|
// If no userId in metadata (common in renewals), find by customerId
|
||||||
|
if (!userId) {
|
||||||
|
console.log('No userId in metadata, finding by customerId');
|
||||||
|
userId = await this.findUserIdByCustomerId(customerId);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
console.error('<< No userId found, this should not happen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const periodStart = this.getPeriodStart(subscription);
|
||||||
|
const periodEnd = this.getPeriodEnd(subscription);
|
||||||
|
const trialStart = subscription.trial_start
|
||||||
|
? new Date(subscription.trial_start * 1000)
|
||||||
|
: null;
|
||||||
|
const trialEnd = subscription.trial_end
|
||||||
|
? new Date(subscription.trial_end * 1000)
|
||||||
|
: null;
|
||||||
|
const currentDate = new Date();
|
||||||
|
|
||||||
|
// Create payment record with subscription status
|
||||||
|
const db = await getDb();
|
||||||
|
const paymentResult = await db
|
||||||
|
.insert(payment)
|
||||||
|
.values({
|
||||||
|
id: randomUUID(),
|
||||||
|
priceId,
|
||||||
|
type: PaymentTypes.SUBSCRIPTION,
|
||||||
|
userId,
|
||||||
|
customerId,
|
||||||
|
subscriptionId,
|
||||||
|
invoiceId: invoice.id,
|
||||||
|
interval: this.mapStripeIntervalToPlanInterval(subscription),
|
||||||
|
status: this.mapSubscriptionStatusToPaymentStatus(
|
||||||
|
subscription.status
|
||||||
|
), // Use actual subscription status
|
||||||
|
periodStart,
|
||||||
|
periodEnd,
|
||||||
|
cancelAtPeriodEnd: subscription.cancel_at_period_end,
|
||||||
|
trialStart,
|
||||||
|
trialEnd,
|
||||||
|
createdAt: currentDate,
|
||||||
|
updatedAt: currentDate,
|
||||||
|
})
|
||||||
|
.returning({ id: payment.id });
|
||||||
|
|
||||||
|
if (paymentResult.length === 0) {
|
||||||
|
console.warn('<< Failed to create subscription payment record');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subscription credits if enabled
|
||||||
|
if (websiteConfig.credits?.enableCredits) {
|
||||||
|
await addSubscriptionCredits(userId, priceId);
|
||||||
|
console.log('Added subscription credits for invoice:', invoice.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('<< Successfully processed subscription payment');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('<< Create subscription payment error:', error);
|
||||||
|
|
||||||
|
// Don't throw error if it's already processed
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes('unique constraint')
|
||||||
|
) {
|
||||||
|
console.log('<< Subscription payment already processed:', invoice.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create one-time payment record and process benefits - NEW ARCHITECTURE
|
||||||
|
*
|
||||||
|
* The order of events may be:
|
||||||
|
* checkout.session.completed
|
||||||
|
* invoice.payment_succeeded
|
||||||
|
*
|
||||||
|
* @param invoice Stripe invoice
|
||||||
|
*/
|
||||||
|
private async createOneTimePayment(invoice: Stripe.Invoice): Promise<void> {
|
||||||
|
console.log('>> Create one-time payment record for invoice:', invoice.id);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const customerId = invoice.customer as string;
|
||||||
|
const paymentIntentId = invoice.payment_intent as string;
|
||||||
|
|
||||||
|
if (!paymentIntentId) {
|
||||||
|
console.warn('<< No payment_intent found in invoice:', invoice.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get payment intent to access metadata
|
||||||
|
const paymentIntent =
|
||||||
|
await this.stripe.paymentIntents.retrieve(paymentIntentId);
|
||||||
|
const metadata = paymentIntent.metadata;
|
||||||
|
|
||||||
|
// Get userId from payment intent metadata or fallback to customerId lookup
|
||||||
|
let userId: string | undefined = metadata?.userId;
|
||||||
|
if (!userId) {
|
||||||
|
console.log('No userId in metadata, finding by customerId');
|
||||||
|
userId = await this.findUserIdByCustomerId(customerId);
|
||||||
|
|
||||||
|
if (!userId) {
|
||||||
|
console.error('<< No userId found, this should not happen');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a credit purchase
|
||||||
|
const isCreditPurchase = metadata?.type === 'credit_purchase';
|
||||||
|
|
||||||
|
if (isCreditPurchase) {
|
||||||
|
// Process credit purchase
|
||||||
|
await this.createCreditPurchasePayment(invoice, metadata, userId);
|
||||||
|
} else {
|
||||||
|
// Process lifetime plan purchase
|
||||||
|
await this.createLifetimePlanPayment(
|
||||||
|
invoice,
|
||||||
|
metadata,
|
||||||
|
userId,
|
||||||
|
customerId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('<< Successfully created one-time payment record');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('<< Create one-time payment error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create payment record for credit purchase - NEW ARCHITECTURE
|
||||||
|
* @param invoice Stripe invoice
|
||||||
|
* @param metadata Payment intent metadata
|
||||||
|
* @param userId User ID
|
||||||
|
*/
|
||||||
|
private async createCreditPurchasePayment(
|
||||||
|
invoice: Stripe.Invoice,
|
||||||
|
metadata: { [key: string]: string },
|
||||||
|
userId: string
|
||||||
|
): Promise<void> {
|
||||||
|
console.log('>> Create credit purchase payment record');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packageId = metadata.packageId;
|
||||||
|
const credits = metadata.credits;
|
||||||
|
const customerId = invoice.customer as string;
|
||||||
|
|
||||||
|
if (!packageId || !credits) {
|
||||||
|
console.warn('<< Missing packageId or credits in metadata');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get credit package
|
||||||
|
const creditPackage = getCreditPackageById(packageId);
|
||||||
|
if (!creditPackage) {
|
||||||
|
console.warn('<< Credit package not found:', packageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create payment record
|
||||||
|
const db = await getDb();
|
||||||
|
const currentDate = new Date();
|
||||||
|
const paymentResult = await db
|
||||||
|
.insert(payment)
|
||||||
|
.values({
|
||||||
|
id: randomUUID(),
|
||||||
|
priceId: metadata.priceId || '',
|
||||||
|
type: PaymentTypes.ONE_TIME,
|
||||||
|
userId,
|
||||||
|
customerId,
|
||||||
|
invoiceId: invoice.id,
|
||||||
|
status: 'completed',
|
||||||
|
periodStart: currentDate,
|
||||||
|
createdAt: currentDate,
|
||||||
|
updatedAt: currentDate,
|
||||||
|
})
|
||||||
|
.returning({ id: payment.id });
|
||||||
|
|
||||||
|
if (paymentResult.length === 0) {
|
||||||
|
console.warn('<< Failed to create credit purchase payment record');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add credits to user account
|
||||||
|
const amount = invoice.amount_paid ? invoice.amount_paid / 100 : 0;
|
||||||
|
await addCredits({
|
||||||
|
userId,
|
||||||
|
amount: Number.parseInt(credits),
|
||||||
|
type: CREDIT_TRANSACTION_TYPE.PURCHASE_PACKAGE,
|
||||||
|
description: `+${credits} credits for package ${packageId} ($${amount.toLocaleString()})`,
|
||||||
|
paymentId: invoice.id, // Use invoice ID as payment ID
|
||||||
|
expireDays: creditPackage.expireDays,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('<< Successfully added credits to user for credit purchase');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('<< Create credit purchase payment error:', error);
|
||||||
|
|
||||||
|
// Don't throw error if it's already processed
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes('unique constraint')
|
||||||
|
) {
|
||||||
|
console.log('<< Credit purchase already processed:', invoice.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create payment record for lifetime plan purchase - NEW ARCHITECTURE
|
||||||
|
* @param invoice Stripe invoice
|
||||||
|
* @param metadata Payment intent metadata
|
||||||
|
* @param userId User ID
|
||||||
|
* @param customerId Customer ID
|
||||||
|
*/
|
||||||
|
private async createLifetimePlanPayment(
|
||||||
|
invoice: Stripe.Invoice,
|
||||||
|
metadata: { [key: string]: string },
|
||||||
|
userId: string,
|
||||||
|
customerId: string
|
||||||
|
): Promise<void> {
|
||||||
|
console.log('>> Create lifetime plan payment record');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const priceId = metadata?.priceId;
|
||||||
|
if (!priceId) {
|
||||||
|
console.warn('<< No priceId found in payment intent metadata');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create payment record
|
||||||
|
const db = await getDb();
|
||||||
|
const currentDate = new Date();
|
||||||
|
const paymentResult = await db
|
||||||
|
.insert(payment)
|
||||||
|
.values({
|
||||||
|
id: randomUUID(),
|
||||||
|
priceId,
|
||||||
|
type: PaymentTypes.ONE_TIME,
|
||||||
|
userId,
|
||||||
|
customerId,
|
||||||
|
invoiceId: invoice.id,
|
||||||
|
status: 'completed',
|
||||||
|
periodStart: currentDate,
|
||||||
|
createdAt: currentDate,
|
||||||
|
updatedAt: currentDate,
|
||||||
|
})
|
||||||
|
.returning({ id: payment.id });
|
||||||
|
|
||||||
|
if (paymentResult.length === 0) {
|
||||||
|
console.warn('<< Failed to create lifetime plan payment record');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add lifetime credits if enabled
|
||||||
|
if (websiteConfig.credits?.enableCredits) {
|
||||||
|
await addLifetimeMonthlyCredits(userId, priceId);
|
||||||
|
console.log('Added lifetime credits for invoice:', invoice.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send notification
|
||||||
|
const amount = invoice.amount_paid ? invoice.amount_paid / 100 : 0;
|
||||||
|
await sendNotification(invoice.id, customerId, userId, amount);
|
||||||
|
|
||||||
|
console.log('<< Successfully created lifetime plan payment record');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('<< Create lifetime plan payment error:', error);
|
||||||
|
|
||||||
|
// Don't throw error if it's already processed
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
error.message.includes('unique constraint')
|
||||||
|
) {
|
||||||
|
console.log('<< Lifetime plan payment already processed:', invoice.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle subscription creation - NEW ARCHITECTURE
|
||||||
|
* Only log the event, payment records created in invoice.payment_succeeded
|
||||||
* @param stripeSubscription Stripe subscription
|
* @param stripeSubscription Stripe subscription
|
||||||
*/
|
*/
|
||||||
private async onCreateSubscription(
|
private async onCreateSubscription(
|
||||||
stripeSubscription: Stripe.Subscription
|
stripeSubscription: Stripe.Subscription
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log('>> Create payment record for Stripe subscription');
|
console.log('Handle subscription creation:', stripeSubscription.id);
|
||||||
const customerId = stripeSubscription.customer as string;
|
|
||||||
|
|
||||||
// get priceId from subscription items (this is always available)
|
|
||||||
const priceId = stripeSubscription.items.data[0]?.price.id;
|
|
||||||
if (!priceId) {
|
|
||||||
console.warn('No priceId found for subscription');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get userId from metadata, we add it in the createCheckout session
|
|
||||||
const userId = stripeSubscription.metadata.userId;
|
|
||||||
if (!userId) {
|
|
||||||
console.warn('No userId found for subscription');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const periodStart = this.getPeriodStart(stripeSubscription);
|
|
||||||
const periodEnd = this.getPeriodEnd(stripeSubscription);
|
|
||||||
|
|
||||||
// create fields
|
|
||||||
const createFields: any = {
|
|
||||||
id: randomUUID(),
|
|
||||||
priceId: priceId,
|
|
||||||
type: PaymentTypes.SUBSCRIPTION,
|
|
||||||
userId: userId,
|
|
||||||
customerId: customerId,
|
|
||||||
subscriptionId: stripeSubscription.id,
|
|
||||||
interval: this.mapStripeIntervalToPlanInterval(stripeSubscription),
|
|
||||||
status: this.mapSubscriptionStatusToPaymentStatus(
|
|
||||||
stripeSubscription.status
|
|
||||||
),
|
|
||||||
periodStart: periodStart,
|
|
||||||
periodEnd: periodEnd,
|
|
||||||
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
|
||||||
trialStart: stripeSubscription.trial_start
|
|
||||||
? new Date(stripeSubscription.trial_start * 1000)
|
|
||||||
: null,
|
|
||||||
trialEnd: stripeSubscription.trial_end
|
|
||||||
? new Date(stripeSubscription.trial_end * 1000)
|
|
||||||
: null,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const db = await getDb();
|
|
||||||
const result = await db
|
|
||||||
.insert(payment)
|
|
||||||
.values(createFields)
|
|
||||||
.returning({ id: payment.id });
|
|
||||||
|
|
||||||
if (result.length > 0) {
|
|
||||||
console.log('<< Created new payment record for Stripe subscription');
|
|
||||||
} else {
|
|
||||||
console.warn('<< No payment record created for Stripe subscription');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conditionally handle credits after subscription creation if enables credits
|
|
||||||
if (websiteConfig.credits?.enableCredits) {
|
|
||||||
await addSubscriptionCredits(userId, priceId);
|
|
||||||
console.log('<< Added subscription monthly credits for user');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update payment record
|
* Update payment record
|
||||||
|
*
|
||||||
|
* When subscription is renewed, the order of events may be:
|
||||||
|
* customer.subscription.updated
|
||||||
|
* invoice.payment_succeeded
|
||||||
|
*
|
||||||
|
* In this case, we need to update the payment record.
|
||||||
|
*
|
||||||
* @param stripeSubscription Stripe subscription
|
* @param stripeSubscription Stripe subscription
|
||||||
*/
|
*/
|
||||||
private async onUpdateSubscription(
|
private async onUpdateSubscription(
|
||||||
stripeSubscription: Stripe.Subscription
|
stripeSubscription: Stripe.Subscription
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log('>> Update payment record for Stripe subscription');
|
console.log('>> Handle subscription update');
|
||||||
|
|
||||||
// get priceId from subscription items (this is always available)
|
// get priceId from subscription items (this is always available)
|
||||||
const priceId = stripeSubscription.items.data[0]?.price.id;
|
const priceId = stripeSubscription.items.data[0]?.price.id;
|
||||||
if (!priceId) {
|
if (!priceId) {
|
||||||
console.warn('No priceId found for subscription');
|
console.warn('<< No priceId found for subscription');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current payment record to check for period changes (indicating renewal)
|
|
||||||
const db = await getDb();
|
|
||||||
const payments = await db
|
|
||||||
.select({
|
|
||||||
userId: payment.userId,
|
|
||||||
periodStart: payment.periodStart,
|
|
||||||
periodEnd: payment.periodEnd,
|
|
||||||
})
|
|
||||||
.from(payment)
|
|
||||||
.where(eq(payment.subscriptionId, stripeSubscription.id))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
// get new period start and end
|
// get new period start and end
|
||||||
const newPeriodStart = this.getPeriodStart(stripeSubscription);
|
const newPeriodStart = this.getPeriodStart(stripeSubscription);
|
||||||
const newPeriodEnd = this.getPeriodEnd(stripeSubscription);
|
const newPeriodEnd = this.getPeriodEnd(stripeSubscription);
|
||||||
|
const trialStart = stripeSubscription.trial_start
|
||||||
// Check if this is a renewal (period has changed and subscription is active)
|
? new Date(stripeSubscription.trial_start * 1000)
|
||||||
const isRenewal =
|
: undefined;
|
||||||
payments.length > 0 &&
|
const trialEnd = stripeSubscription.trial_end
|
||||||
stripeSubscription.status === 'active' &&
|
? new Date(stripeSubscription.trial_end * 1000)
|
||||||
payments[0].periodStart &&
|
: undefined;
|
||||||
newPeriodStart &&
|
|
||||||
payments[0].periodStart.getTime() !== newPeriodStart.getTime();
|
|
||||||
|
|
||||||
// update fields
|
// update fields
|
||||||
const updateFields: any = {
|
const updateFields: any = {
|
||||||
@ -634,15 +950,12 @@ export class StripeProvider implements PaymentProvider {
|
|||||||
periodStart: newPeriodStart,
|
periodStart: newPeriodStart,
|
||||||
periodEnd: newPeriodEnd,
|
periodEnd: newPeriodEnd,
|
||||||
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end,
|
||||||
trialStart: stripeSubscription.trial_start
|
trialStart: trialStart,
|
||||||
? new Date(stripeSubscription.trial_start * 1000)
|
trialEnd: trialEnd,
|
||||||
: undefined,
|
|
||||||
trialEnd: stripeSubscription.trial_end
|
|
||||||
? new Date(stripeSubscription.trial_end * 1000)
|
|
||||||
: undefined,
|
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const db = await getDb();
|
||||||
const result = await db
|
const result = await db
|
||||||
.update(payment)
|
.update(payment)
|
||||||
.set(updateFields)
|
.set(updateFields)
|
||||||
@ -650,24 +963,9 @@ export class StripeProvider implements PaymentProvider {
|
|||||||
.returning({ id: payment.id });
|
.returning({ id: payment.id });
|
||||||
|
|
||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
console.log('<< Updated payment record for Stripe subscription');
|
console.log('<< Updated payment record for subscription');
|
||||||
|
|
||||||
// Add credits for subscription renewal
|
|
||||||
const currentPayment = payments[0];
|
|
||||||
const userId = currentPayment.userId;
|
|
||||||
// Add subscription renewal credits if plan config enables credits
|
|
||||||
if (isRenewal && userId && websiteConfig.credits?.enableCredits) {
|
|
||||||
// Note: For yearly subscriptions, this webhook only triggers once per year
|
|
||||||
// Monthly credits for yearly subscribers are handled by the distributeCreditsToAllUsers cron job
|
|
||||||
await addSubscriptionCredits(userId, priceId);
|
|
||||||
console.log('<< Added subscription renewal credits for user');
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
'<< No renewal credits added for user, isRenewal: ' + isRenewal
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('<< No payment record found for Stripe subscription');
|
console.warn('<< No payment record found for subscription update');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -678,7 +976,8 @@ export class StripeProvider implements PaymentProvider {
|
|||||||
private async onDeleteSubscription(
|
private async onDeleteSubscription(
|
||||||
stripeSubscription: Stripe.Subscription
|
stripeSubscription: Stripe.Subscription
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log('>> Mark payment record for Stripe subscription as canceled');
|
console.log('>> Handle subscription deletion');
|
||||||
|
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const result = await db
|
const result = await db
|
||||||
.update(payment)
|
.update(payment)
|
||||||
@ -694,177 +993,30 @@ export class StripeProvider implements PaymentProvider {
|
|||||||
if (result.length > 0) {
|
if (result.length > 0) {
|
||||||
console.log('<< Marked payment record for subscription as canceled');
|
console.log('<< Marked payment record for subscription as canceled');
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn('<< No payment record found for subscription deletion');
|
||||||
'<< No payment record found to cancel for Stripe subscription'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle one-time payment
|
* Handle checkout session completion - NEW ARCHITECTURE
|
||||||
|
* Only log the event, payment records created in invoice.payment_succeeded
|
||||||
* @param session Stripe checkout session
|
* @param session Stripe checkout session
|
||||||
*/
|
*/
|
||||||
private async onOnetimePayment(
|
private async onOnetimePayment(
|
||||||
session: Stripe.Checkout.Session
|
session: Stripe.Checkout.Session
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const customerId = session.customer as string;
|
console.log('Handle checkout session completion:', session.id);
|
||||||
console.log('>> Handle onetime payment for customer');
|
|
||||||
|
|
||||||
// get userId from session metadata, we add it in the createCheckout session
|
|
||||||
const userId = session.metadata?.userId;
|
|
||||||
if (!userId) {
|
|
||||||
console.warn('No userId found for checkout session');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get priceId from session metadata, not from line items
|
|
||||||
// const priceId = session.line_items?.data[0]?.price?.id;
|
|
||||||
const priceId = session.metadata?.priceId;
|
|
||||||
if (!priceId) {
|
|
||||||
console.warn('No priceId found for checkout session');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const db = await getDb();
|
|
||||||
|
|
||||||
// Check if this session has already been processed to prevent duplicate processing
|
|
||||||
const existingPayment = await db
|
|
||||||
.select({ id: payment.id })
|
|
||||||
.from(payment)
|
|
||||||
.where(eq(payment.sessionId, session.id))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingPayment.length > 0) {
|
|
||||||
console.log(
|
|
||||||
'One-time payment session already processed: ' + session.id
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a one-time payment record
|
|
||||||
const now = new Date();
|
|
||||||
const result = await db
|
|
||||||
.insert(payment)
|
|
||||||
.values({
|
|
||||||
id: randomUUID(),
|
|
||||||
priceId: priceId,
|
|
||||||
type: PaymentTypes.ONE_TIME,
|
|
||||||
userId: userId,
|
|
||||||
customerId: customerId,
|
|
||||||
sessionId: session.id, // Track the session ID
|
|
||||||
status: 'completed', // One-time payments are always completed
|
|
||||||
periodStart: now,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
})
|
|
||||||
.returning({ id: payment.id });
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
console.warn('<< Failed to create one-time payment record for user');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('Created one-time payment record for user');
|
|
||||||
|
|
||||||
// Conditionally handle credits after one-time payment
|
|
||||||
if (websiteConfig.credits?.enableCredits) {
|
|
||||||
// For now, one time payment is only for lifetime plan
|
|
||||||
await addLifetimeMonthlyCredits(userId, priceId);
|
|
||||||
console.log('<< Added lifetime monthly credits for user');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send notification
|
|
||||||
const amount = session.amount_total ? session.amount_total / 100 : 0;
|
|
||||||
await sendNotification(session.id, customerId, userId, amount);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('onOnetimePayment error for session: ' + session.id, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle credit purchase
|
* Handle credit purchase checkout completion - NEW ARCHITECTURE
|
||||||
|
* Only log the event, payment records created in invoice.payment_succeeded
|
||||||
* @param session Stripe checkout session
|
* @param session Stripe checkout session
|
||||||
*/
|
*/
|
||||||
private async onCreditPurchase(
|
private async onCreditPurchase(
|
||||||
session: Stripe.Checkout.Session
|
session: Stripe.Checkout.Session
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const customerId = session.customer as string;
|
console.log('Handle credit purchase checkout completion:', session.id);
|
||||||
console.log('>> Handle credit purchase for customer');
|
|
||||||
|
|
||||||
// get userId from session metadata, we add it in the createCheckout session
|
|
||||||
const userId = session.metadata?.userId;
|
|
||||||
if (!userId) {
|
|
||||||
console.warn('No userId found for checkout session');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get packageId from session metadata
|
|
||||||
const packageId = session.metadata?.packageId;
|
|
||||||
if (!packageId) {
|
|
||||||
console.warn('No packageId found for checkout session');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get credits from session metadata
|
|
||||||
const credits = session.metadata?.credits;
|
|
||||||
if (!credits) {
|
|
||||||
console.warn('No credits found for checkout session');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get credit package
|
|
||||||
const creditPackage = getCreditPackageById(packageId);
|
|
||||||
if (!creditPackage) {
|
|
||||||
console.warn('Credit package ' + packageId + ' not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if this session has already been processed to prevent duplicate credit additions
|
|
||||||
const db = await getDb();
|
|
||||||
const existingPayment = await db
|
|
||||||
.select({ id: payment.id })
|
|
||||||
.from(payment)
|
|
||||||
.where(eq(payment.sessionId, session.id))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingPayment.length > 0) {
|
|
||||||
console.log('Credit purchase session already processed: ' + session.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create payment record first to mark this session as processed
|
|
||||||
const now = new Date();
|
|
||||||
await db.insert(payment).values({
|
|
||||||
id: randomUUID(),
|
|
||||||
priceId: session.metadata?.priceId || '',
|
|
||||||
type: PaymentTypes.ONE_TIME,
|
|
||||||
userId: userId,
|
|
||||||
customerId: customerId,
|
|
||||||
sessionId: session.id, // Use sessionId to track processed sessions
|
|
||||||
status: 'completed',
|
|
||||||
periodStart: now,
|
|
||||||
createdAt: now,
|
|
||||||
updatedAt: now,
|
|
||||||
});
|
|
||||||
|
|
||||||
// add credits to user account
|
|
||||||
const amount = session.amount_total ? session.amount_total / 100 : 0;
|
|
||||||
await addCredits({
|
|
||||||
userId,
|
|
||||||
amount: Number.parseInt(credits),
|
|
||||||
type: CREDIT_TRANSACTION_TYPE.PURCHASE_PACKAGE,
|
|
||||||
description: `+${credits} credits for package ${packageId} ($${amount.toLocaleString()})`,
|
|
||||||
paymentId: session.id,
|
|
||||||
expireDays: creditPackage.expireDays,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Added ' + credits + ' credits to user');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('onCreditPurchase error for session: ' + session.id, error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user