diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b78848946..39066306b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -187,7 +187,7 @@ importers:
version: 13.0.1
stripe:
specifier: ^20.4.1
- version: 20.4.1(@types/node@24.10.13)
+ version: 20.4.1(@types/node@25.6.0)
thenby:
specifier: ^1.4.0
version: 1.4.0
@@ -316,6 +316,42 @@ importers:
specifier: ^6.0.3
version: 6.0.3
+ dist:
+ dependencies:
+ '@tanstack/react-query':
+ specifier: ^4.33.0
+ version: 4.44.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ classnames:
+ specifier: ^2.3.1
+ version: 2.5.1
+ colord:
+ specifier: ^2.9.2
+ version: 2.9.3
+ immer:
+ specifier: ^9.0.12
+ version: 9.0.21
+ moment-timezone:
+ specifier: ^0.5.35
+ version: 0.5.48
+ next:
+ specifier: ^13.4.0
+ version: 13.5.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ next-basics:
+ specifier: ^0.36.0
+ version: 0.36.0(next@13.5.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ react:
+ specifier: ^18.2.0
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.3.1(react@18.3.1)
+ react-intl:
+ specifier: ^5.24.7
+ version: 5.25.1(react@18.3.1)(typescript@6.0.3)
+ zustand:
+ specifier: ^4.3.8
+ version: 4.5.7(@types/react@18.3.28)(immer@9.0.21)(react@18.3.1)
+
packages:
'@ampproject/remapping@2.3.0':
@@ -568,24 +604,28 @@ packages:
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@biomejs/cli-linux-arm64@2.4.12':
resolution: {integrity: sha512-tOwuCuZZtKi1jVzbk/5nXmIsziOB6yqN8c9r9QM0EJYPU6DpQWf11uBOSCfFKKM4H3d9ZoarvlgMfbcuD051Pw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@biomejs/cli-linux-x64-musl@2.4.12':
resolution: {integrity: sha512-dwTIgZrGutzhkQCuvHynCkyW6hJxUuyZqKKO0YNfaS2GUoRO+tOvxXZqZB6SkWAOdfZTzwaw8IEdUnIkHKHoew==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@biomejs/cli-linux-x64@2.4.12':
resolution: {integrity: sha512-8pFeAnLU9QdW9jCIslB/v82bI0lhBmz2ZAKc8pVMFPO0t0wAHsoEkrUQUbMkIorTRIjbqyNZHA3lEXavsPWYSw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@biomejs/cli-win32-arm64@2.4.12':
resolution: {integrity: sha512-B0DLnx0vA9ya/3v7XyCaP+/lCpnbWbMOfUFFve+xb5OxyYvdHaS55YsSddr228Y+JAFk58agCuZTsqNiw2a6ig==}
@@ -1475,18 +1515,47 @@ packages:
cpu: [x64]
os: [win32]
+ '@formatjs/ecma402-abstract@1.11.4':
+ resolution: {integrity: sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==}
+
+ '@formatjs/fast-memoize@1.2.1':
+ resolution: {integrity: sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==}
+
'@formatjs/fast-memoize@3.1.2':
resolution: {integrity: sha512-vPnriihkfK0lzoQGaXq+qXH23VsYyansRTkTgo2aTG0k1NjLFyZimFVdfj4C9JkSE5dm7CEngcQ5TTc1yAyBfQ==}
+ '@formatjs/icu-messageformat-parser@2.1.0':
+ resolution: {integrity: sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==}
+
'@formatjs/icu-messageformat-parser@3.5.4':
resolution: {integrity: sha512-JVY39ROgLt+pIYngo6piyj4OVfZmXs/2FkC4wLS+ql1Eig/sGJKB7YwDO/5bkJFkfwaFAeIpgEiJc8hiYxNalw==}
+ '@formatjs/icu-skeleton-parser@1.3.6':
+ resolution: {integrity: sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==}
+
'@formatjs/icu-skeleton-parser@2.1.4':
resolution: {integrity: sha512-8bSFZbrlvGX11ywMZxtgkPBt5Q8/etyts7j7j+GWpOVK1g43zwMIH3LZxk43HAtEP7L/jtZ+OZaMiFTOiBj9CA==}
+ '@formatjs/intl-displaynames@5.4.3':
+ resolution: {integrity: sha512-4r12A3mS5dp5hnSaQCWBuBNfi9Amgx2dzhU4lTFfhSxgb5DOAiAbMpg6+7gpWZgl4ahsj3l2r/iHIjdmdXOE2Q==}
+
+ '@formatjs/intl-listformat@6.5.3':
+ resolution: {integrity: sha512-ozpz515F/+3CU+HnLi5DYPsLa6JoCfBggBSSg/8nOB5LYSFW9+ZgNQJxJ8tdhKYeODT+4qVHX27EeJLoxLGLNg==}
+
+ '@formatjs/intl-localematcher@0.2.25':
+ resolution: {integrity: sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==}
+
'@formatjs/intl-localematcher@0.8.3':
resolution: {integrity: sha512-pHUjWb9NuhnMs8+PxQdzBtZRFJHlGhrURGAbm6Ltwl82BFajeuiIR3jblSa7ia3r62rXe/0YtVpUG3xWr5bFCA==}
+ '@formatjs/intl@2.2.1':
+ resolution: {integrity: sha512-vgvyUOOrzqVaOFYzTf2d3+ToSkH2JpR7x/4U1RyoHQLmvEaTQvXJ7A2qm1Iy3brGNXC/+/7bUlc3lpH+h/LOJA==}
+ peerDependencies:
+ typescript: ^4.5
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
'@hello-pangea/dnd@18.0.1':
resolution: {integrity: sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==}
peerDependencies:
@@ -1529,89 +1598,105 @@ packages:
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-ppc64@1.2.4':
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-riscv64@1.2.4':
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-s390x@1.2.4':
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-ppc64@0.34.5':
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-riscv64@0.34.5':
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-s390x@0.34.5':
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@img/sharp-wasm32@0.34.5':
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
@@ -1793,44 +1878,97 @@ packages:
resolution: {integrity: sha512-4kJvV+wrt4/ncehfaOXfhv1FYKCv9EFjq6/tgXXYSlqPDcbsuTXR0DTz9IvyZ/CpPRadRD/zzIqD9wXgCmA2lg==}
engines: {node: '>=18.0.0'}
+ '@next/env@13.5.11':
+ resolution: {integrity: sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==}
+
'@next/env@16.2.4':
resolution: {integrity: sha512-dKkkOzOSwFYe5RX6y26fZgkSpVAlIOJKQHIiydQcrWH6y/97+RceSOAdjZ14Qa3zLduVUy0TXcn+EiM6t4rPgw==}
+ '@next/swc-darwin-arm64@13.5.9':
+ resolution: {integrity: sha512-pVyd8/1y1l5atQRvOaLOvfbmRwefxLhqQOzYo/M7FQ5eaRwA1+wuCn7t39VwEgDd7Aw1+AIWwd+MURXUeXhwDw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
'@next/swc-darwin-arm64@16.2.4':
resolution: {integrity: sha512-OXTFFox5EKN1Ym08vfrz+OXxmCcEjT4SFMbNRsWZE99dMqt2Kcusl5MqPXcW232RYkMLQTy0hqgAMEsfEd/l2A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
+ '@next/swc-darwin-x64@13.5.9':
+ resolution: {integrity: sha512-DwdeJqP7v8wmoyTWPbPVodTwCybBZa02xjSJ6YQFIFZFZ7dFgrieKW4Eo0GoIcOJq5+JxkQyejmI+8zwDp3pwA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
'@next/swc-darwin-x64@16.2.4':
resolution: {integrity: sha512-XhpVnUfmYWvD3YrXu55XdcAkQtOnvaI6wtQa8fuF5fGoKoxIUZ0kWPtcOfqJEWngFF/lOS9l3+O9CcownhiQxQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
+ '@next/swc-linux-arm64-gnu@13.5.9':
+ resolution: {integrity: sha512-wdQsKsIsGSNdFojvjW3Ozrh8Q00+GqL3wTaMjDkQxVtRbAqfFBtrLPO0IuWChVUP2UeuQcHpVeUvu0YgOP00+g==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
'@next/swc-linux-arm64-gnu@16.2.4':
resolution: {integrity: sha512-Mx/tjlNA3G8kg14QvuGAJ4xBwPk1tUHq56JxZ8CXnZwz1Etz714soCEzGQQzVMz4bEnGPowzkV6Xrp6wAkEWOQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
+
+ '@next/swc-linux-arm64-musl@13.5.9':
+ resolution: {integrity: sha512-6VpS+bodQqzOeCwGxoimlRoosiWlSc0C224I7SQWJZoyJuT1ChNCo+45QQH+/GtbR/s7nhaUqmiHdzZC9TXnXA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
'@next/swc-linux-arm64-musl@16.2.4':
resolution: {integrity: sha512-iVMMp14514u7Nup2umQS03nT/bN9HurK8ufylC3FZNykrwjtx7V1A7+4kvhbDSCeonTVqV3Txnv0Lu+m2oDXNg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
+
+ '@next/swc-linux-x64-gnu@13.5.9':
+ resolution: {integrity: sha512-XxG3yj61WDd28NA8gFASIR+2viQaYZEFQagEodhI/R49gXWnYhiflTeeEmCn7Vgnxa/OfK81h1gvhUZ66lozpw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
'@next/swc-linux-x64-gnu@16.2.4':
resolution: {integrity: sha512-EZOvm1aQWgnI/N/xcWOlnS3RQBk0VtVav5Zo7n4p0A7UKyTDx047k8opDbXgBpHl4CulRqRfbw3QrX2w5UOXMQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
+
+ '@next/swc-linux-x64-musl@13.5.9':
+ resolution: {integrity: sha512-/dnscWqfO3+U8asd+Fc6dwL2l9AZDl7eKtPNKW8mKLh4Y4wOpjJiamhe8Dx+D+Oq0GYVjuW0WwjIxYWVozt2bA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
'@next/swc-linux-x64-musl@16.2.4':
resolution: {integrity: sha512-h9FxsngCm9cTBf71AR4fGznDEDx1hS7+kSEiIRjq5kO1oXWm07DxVGZjCvk0SGx7TSjlUqhI8oOyz7NfwAdPoA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
+
+ '@next/swc-win32-arm64-msvc@13.5.9':
+ resolution: {integrity: sha512-T/iPnyurOK5a4HRUcxAlss8uzoEf5h9tkd+W2dSWAfzxv8WLKlUgbfk+DH43JY3Gc2xK5URLuXrxDZ2mGfk/jw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
'@next/swc-win32-arm64-msvc@16.2.4':
resolution: {integrity: sha512-3NdJV5OXMSOeJYijX+bjaLge3mJBlh4ybydbT4GFoB/2hAojWHtMhl3CYlYoMrjPuodp0nzFVi4Tj2+WaMg+Ow==}
@@ -1838,6 +1976,18 @@ packages:
cpu: [arm64]
os: [win32]
+ '@next/swc-win32-ia32-msvc@13.5.9':
+ resolution: {integrity: sha512-BLiPKJomaPrTAb7ykjA0LPcuuNMLDVK177Z1xe0nAem33+9FIayU4k/OWrtSn9SAJW/U60+1hoey5z+KCHdRLQ==}
+ engines: {node: '>= 10'}
+ cpu: [ia32]
+ os: [win32]
+
+ '@next/swc-win32-x64-msvc@13.5.9':
+ resolution: {integrity: sha512-/72/dZfjXXNY/u+n8gqZDjI6rxKMpYsgBBYNZKWOQw0BpBF7WCnPflRy3ZtvQ2+IYI3ZH2bPyj7K+6a6wNk90Q==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
'@next/swc-win32-x64-msvc@16.2.4':
resolution: {integrity: sha512-kMVGgsqhO5YTYODD9IPGGhA6iprWidQckK3LmPeW08PIFENRmgfb4MjXHO+p//d+ts2rpjvK5gXWzXSMrPl9cw==}
engines: {node: '>= 10'}
@@ -1885,36 +2035,42 @@ packages:
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@parcel/watcher-linux-arm-musl@2.5.6':
resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@parcel/watcher-linux-arm64-glibc@2.5.6':
resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@parcel/watcher-linux-arm64-musl@2.5.6':
resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@parcel/watcher-linux-x64-glibc@2.5.6':
resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@parcel/watcher-linux-x64-musl@2.5.6':
resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@parcel/watcher-win32-arm64@2.5.6':
resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
@@ -2312,66 +2468,79 @@ packages:
resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==}
cpu: [arm]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.60.1':
resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==}
cpu: [arm]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.60.1':
resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.60.1':
resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.60.1':
resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==}
cpu: [loong64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.60.1':
resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==}
cpu: [loong64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.60.1':
resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.60.1':
resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==}
cpu: [ppc64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.60.1':
resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.60.1':
resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.60.1':
resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.60.1':
resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.60.1':
resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@rollup/rollup-openbsd-x64@4.60.1':
resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==}
@@ -2533,36 +2702,42 @@ packages:
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@swc/core-linux-arm64-musl@1.15.26':
resolution: {integrity: sha512-iNlbvTIo425rkKzDLLWFJGnFXr3myETUdIDHcjuiPNZE8b0ogmcAuilC4yEJX7FSHGbnlsoJcCT2xf4b3VJmmQ==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@swc/core-linux-ppc64-gnu@1.15.26':
resolution: {integrity: sha512-AuuEOtG+YXKIjIUup4RsxYNklx6XVB3WKWfhxG6hnfPrn7vp89RNOLbbyyprgj6Sk7k9ulwGVTJElEvmBNPSCA==}
engines: {node: '>=10'}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@swc/core-linux-s390x-gnu@1.15.26':
resolution: {integrity: sha512-JcMDWQvW1BchUyRg8E0jHiTx7CQYpUr5uDEL1dnPDECrEjBEGG2ynmJ3XX70sWXql0JagqR1t3VpANYFWdUnqA==}
engines: {node: '>=10'}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@swc/core-linux-x64-gnu@1.15.26':
resolution: {integrity: sha512-FW7V7Mbpq4+PA7BiAq76LJs8MdNuUSylyuRVfQRkhIyeWadFroZ+KOPgjku8Z/fXzngxBRvsk+PGGB0t8mGcjA==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@swc/core-linux-x64-musl@1.15.26':
resolution: {integrity: sha512-w8erqMHsVcdGwUfJxF6LaiTuPoKnyLOcUbhLcxiXrlLt5MLjtlgcIeUY/NWK/oPoyqkgH+/i8pOJnMTxvl83ZQ==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@swc/core-win32-arm64-msvc@1.15.26':
resolution: {integrity: sha512-uDCWCNpUiqkbvPmsuPUTn/P7ag9SqNXD2JT/W3dUu7yZ2krzN+nmmoQ2xRX63/J6RYiHI7aT4jo7Z++lsljlPA==}
@@ -2600,15 +2775,33 @@ packages:
'@swc/helpers@0.5.18':
resolution: {integrity: sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==}
+ '@swc/helpers@0.5.2':
+ resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==}
+
'@swc/helpers@0.5.21':
resolution: {integrity: sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==}
'@swc/types@0.1.26':
resolution: {integrity: sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==}
+ '@tanstack/query-core@4.44.0':
+ resolution: {integrity: sha512-swSgb7OiPRR3UuIL7NuDrZNSMGmQD+wdtHxPD7j60SvBEnxbXurl5XOirtGEX2gm2hbK6mC8kMV1I+uO3l0UOw==}
+
'@tanstack/query-core@5.99.0':
resolution: {integrity: sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ==}
+ '@tanstack/react-query@4.44.0':
+ resolution: {integrity: sha512-RuIqHYrS98LrK/8kJJOJMMSQ/BCpojwsXDh7p0fBmp38ZOz6dlk+uyFRRusH+V+t3POoCsDOQ2zhomEYOeReXw==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-native: '*'
+ peerDependenciesMeta:
+ react-dom:
+ optional: true
+ react-native:
+ optional: true
+
'@tanstack/react-query@5.99.0':
resolution: {integrity: sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw==}
peerDependencies:
@@ -2663,6 +2856,11 @@ packages:
'@types/glob@7.2.0':
resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==}
+ '@types/hoist-non-react-statics@3.3.7':
+ resolution: {integrity: sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==}
+ peerDependencies:
+ '@types/react': '*'
+
'@types/istanbul-lib-coverage@2.0.6':
resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==}
@@ -2688,6 +2886,9 @@ packages:
'@types/pg@8.20.0':
resolution: {integrity: sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==}
+ '@types/prop-types@15.7.15':
+ resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==}
+
'@types/react-dom@19.2.3':
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
peerDependencies:
@@ -2778,41 +2979,49 @@ packages:
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
cpu: [arm64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
cpu: [arm64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
cpu: [ppc64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
cpu: [riscv64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
cpu: [riscv64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
cpu: [s390x]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
cpu: [x64]
os: [linux]
+ libc: [glibc]
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
cpu: [x64]
os: [linux]
+ libc: [musl]
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
@@ -4058,6 +4267,9 @@ packages:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'}
+ hoist-non-react-statics@3.3.2:
+ resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
+
hono@4.12.9:
resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==}
engines: {node: '>=16.9.0'}
@@ -4165,6 +4377,9 @@ packages:
intl-messageformat@11.2.1:
resolution: {integrity: sha512-1gAVEUt3wEPvTqML4Fsw9klZV5j0vszQxayP/fi6gUroAc8AUHiNaisBKLWxybL1AdWq1mP07YV1q8v4N92ilQ==}
+ intl-messageformat@9.13.0:
+ resolution: {integrity: sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==}
+
ipaddr.js@2.3.0:
resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==}
engines: {node: '>= 10'}
@@ -4811,6 +5026,12 @@ packages:
resolution: {integrity: sha512-7e87vk0DdWT647wjcfEtWeMtjm+zVGqNohN/aeIymbUfjHQ2T4Sx5kM+1irVDBSloNC3CkGKxswdMoo8yhqTDg==}
engines: {node: '>=10', npm: '>=6'}
+ moment-timezone@0.5.48:
+ resolution: {integrity: sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==}
+
+ moment@2.30.1:
+ resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
+
motion-dom@12.38.0:
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
@@ -4865,6 +5086,13 @@ packages:
neo-async@2.6.2:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
+ next-basics@0.36.0:
+ resolution: {integrity: sha512-Nwou8pCjFuoD/ZxUw9iKC7hhZeWbo/ng0ze74yck3W89MNc/CepwCDziflAHY5XcmIVNmpXOCu9OfmzTdVRPWQ==}
+ peerDependencies:
+ next: ^13.4.0
+ react: ^18.2.0
+ react-dom: ^18.2.0
+
next-intl-swc-plugin-extractor@4.9.1:
resolution: {integrity: sha512-8whJJ6oxJz8JqkHarggmmuEDyXgC7nEnaPhZD91CJwEWW4xp0AST3Mw17YxvHyP2vAF3taWfFbs1maD+WWtz3w==}
@@ -4878,6 +5106,21 @@ packages:
typescript:
optional: true
+ next@13.5.11:
+ resolution: {integrity: sha512-WUPJ6WbAX9tdC86kGTu92qkrRdgRqVrY++nwM+shmWQwmyxt4zhZfR59moXSI4N8GDYCBY3lIAqhzjDd4rTC8Q==}
+ engines: {node: '>=16.14.0'}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.1.0
+ react: ^18.2.0
+ react-dom: ^18.2.0
+ sass: ^1.3.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ sass:
+ optional: true
+
next@16.2.4:
resolution: {integrity: sha512-kPvz56wF5frc+FxlHI5qnklCzbq53HTwORaWBGdT0vNoKh1Aya9XC8aPauH4NJxqtzbWsS5mAbctm4cr+EkQ2Q==}
engines: {node: '>=20.9.0'}
@@ -5714,6 +5957,11 @@ packages:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
+ react-dom@18.3.1:
+ resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
+ peerDependencies:
+ react: ^18.3.1
+
react-dom@19.2.5:
resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==}
peerDependencies:
@@ -5790,6 +6038,10 @@ packages:
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
+ react@18.3.1:
+ resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
+ engines: {node: '>=0.10.0'}
+
react@19.2.5:
resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==}
engines: {node: '>=0.10.0'}
@@ -6149,6 +6401,10 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
+ streamsearch@1.1.0:
+ resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
+ engines: {node: '>=10.0.0'}
+
string-hash@1.1.3:
resolution: {integrity: sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A==}
@@ -6219,6 +6475,19 @@ packages:
style-inject@0.3.0:
resolution: {integrity: sha512-IezA2qp+vcdlhJaVm5SOdPPTUu0FCEqfNSli2vRuSIBbu5Nq5UvygTk/VzeCqfLz2Atj3dVII5QBKGZRZ0edzw==}
+ styled-jsx@5.1.1:
+ resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==}
+ engines: {node: '>= 12.0.0'}
+ peerDependencies:
+ '@babel/core': '*'
+ babel-plugin-macros: '*'
+ react: '>= 16.8.0 || 17.x.x || ^18.0.0-0'
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ babel-plugin-macros:
+ optional: true
+
styled-jsx@5.1.6:
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
engines: {node: '>= 12.0.0'}
@@ -6579,6 +6848,7 @@ packages:
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+ deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
v8-compile-cache-lib@3.0.1:
@@ -6714,6 +6984,21 @@ packages:
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
+ zustand@4.5.7:
+ resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
+ engines: {node: '>=12.7.0'}
+ peerDependencies:
+ '@types/react': '>=16.8'
+ immer: '>=9.0.6'
+ react: '>=16.8'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+
zustand@5.0.12:
resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==}
engines: {node: '>=12.20.0'}
@@ -7782,18 +8067,66 @@ snapshots:
'@esbuild/win32-x64@0.28.0':
optional: true
+ '@formatjs/ecma402-abstract@1.11.4':
+ dependencies:
+ '@formatjs/intl-localematcher': 0.2.25
+ tslib: 2.8.1
+
+ '@formatjs/fast-memoize@1.2.1':
+ dependencies:
+ tslib: 2.8.1
+
'@formatjs/fast-memoize@3.1.2': {}
+ '@formatjs/icu-messageformat-parser@2.1.0':
+ dependencies:
+ '@formatjs/ecma402-abstract': 1.11.4
+ '@formatjs/icu-skeleton-parser': 1.3.6
+ tslib: 2.8.1
+
'@formatjs/icu-messageformat-parser@3.5.4':
dependencies:
'@formatjs/icu-skeleton-parser': 2.1.4
+ '@formatjs/icu-skeleton-parser@1.3.6':
+ dependencies:
+ '@formatjs/ecma402-abstract': 1.11.4
+ tslib: 2.8.1
+
'@formatjs/icu-skeleton-parser@2.1.4': {}
+ '@formatjs/intl-displaynames@5.4.3':
+ dependencies:
+ '@formatjs/ecma402-abstract': 1.11.4
+ '@formatjs/intl-localematcher': 0.2.25
+ tslib: 2.8.1
+
+ '@formatjs/intl-listformat@6.5.3':
+ dependencies:
+ '@formatjs/ecma402-abstract': 1.11.4
+ '@formatjs/intl-localematcher': 0.2.25
+ tslib: 2.8.1
+
+ '@formatjs/intl-localematcher@0.2.25':
+ dependencies:
+ tslib: 2.8.1
+
'@formatjs/intl-localematcher@0.8.3':
dependencies:
'@formatjs/fast-memoize': 3.1.2
+ '@formatjs/intl@2.2.1(typescript@6.0.3)':
+ dependencies:
+ '@formatjs/ecma402-abstract': 1.11.4
+ '@formatjs/fast-memoize': 1.2.1
+ '@formatjs/icu-messageformat-parser': 2.1.0
+ '@formatjs/intl-displaynames': 5.4.3
+ '@formatjs/intl-listformat': 6.5.3
+ intl-messageformat: 9.13.0
+ tslib: 2.8.1
+ optionalDependencies:
+ typescript: 6.0.3
+
'@hello-pangea/dnd@18.0.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
dependencies:
'@babel/runtime': 7.29.2
@@ -8184,29 +8517,58 @@ snapshots:
'@netlify/plugin-nextjs@5.15.9': {}
+ '@next/env@13.5.11': {}
+
'@next/env@16.2.4': {}
+ '@next/swc-darwin-arm64@13.5.9':
+ optional: true
+
'@next/swc-darwin-arm64@16.2.4':
optional: true
+ '@next/swc-darwin-x64@13.5.9':
+ optional: true
+
'@next/swc-darwin-x64@16.2.4':
optional: true
+ '@next/swc-linux-arm64-gnu@13.5.9':
+ optional: true
+
'@next/swc-linux-arm64-gnu@16.2.4':
optional: true
+ '@next/swc-linux-arm64-musl@13.5.9':
+ optional: true
+
'@next/swc-linux-arm64-musl@16.2.4':
optional: true
+ '@next/swc-linux-x64-gnu@13.5.9':
+ optional: true
+
'@next/swc-linux-x64-gnu@16.2.4':
optional: true
+ '@next/swc-linux-x64-musl@13.5.9':
+ optional: true
+
'@next/swc-linux-x64-musl@16.2.4':
optional: true
+ '@next/swc-win32-arm64-msvc@13.5.9':
+ optional: true
+
'@next/swc-win32-arm64-msvc@16.2.4':
optional: true
+ '@next/swc-win32-ia32-msvc@13.5.9':
+ optional: true
+
+ '@next/swc-win32-x64-msvc@13.5.9':
+ optional: true
+
'@next/swc-win32-x64-msvc@16.2.4':
optional: true
@@ -8882,6 +9244,10 @@ snapshots:
dependencies:
tslib: 2.8.1
+ '@swc/helpers@0.5.2':
+ dependencies:
+ tslib: 2.8.1
+
'@swc/helpers@0.5.21':
dependencies:
tslib: 2.8.1
@@ -8890,8 +9256,18 @@ snapshots:
dependencies:
'@swc/counter': 0.1.3
+ '@tanstack/query-core@4.44.0': {}
+
'@tanstack/query-core@5.99.0': {}
+ '@tanstack/react-query@4.44.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
+ dependencies:
+ '@tanstack/query-core': 4.44.0
+ react: 18.3.1
+ use-sync-external-store: 1.6.0(react@18.3.1)
+ optionalDependencies:
+ react-dom: 18.3.1(react@18.3.1)
+
'@tanstack/react-query@5.99.0(react@19.2.5)':
dependencies:
'@tanstack/query-core': 5.99.0
@@ -8954,6 +9330,11 @@ snapshots:
'@types/minimatch': 6.0.0
'@types/node': 25.6.0
+ '@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)':
+ dependencies:
+ '@types/react': 18.3.28
+ hoist-non-react-statics: 3.3.2
+
'@types/istanbul-lib-coverage@2.0.6': {}
'@types/istanbul-lib-report@3.0.3':
@@ -10521,6 +10902,10 @@ snapshots:
dependencies:
function-bind: 1.1.2
+ hoist-non-react-statics@3.3.2:
+ dependencies:
+ react-is: 16.13.1
+
hono@4.12.9: {}
hosted-git-info@2.8.9: {}
@@ -10609,6 +10994,13 @@ snapshots:
'@formatjs/fast-memoize': 3.1.2
'@formatjs/icu-messageformat-parser': 3.5.4
+ intl-messageformat@9.13.0:
+ dependencies:
+ '@formatjs/ecma402-abstract': 1.11.4
+ '@formatjs/fast-memoize': 1.2.1
+ '@formatjs/icu-messageformat-parser': 2.1.0
+ tslib: 2.8.1
+
ipaddr.js@2.3.0: {}
is-array-buffer@3.0.5:
@@ -11428,6 +11820,12 @@ snapshots:
mmdb-lib@3.0.2: {}
+ moment-timezone@0.5.48:
+ dependencies:
+ moment: 2.30.1
+
+ moment@2.30.1: {}
+
motion-dom@12.38.0:
dependencies:
motion-utils: 12.36.0
@@ -11476,6 +11874,15 @@ snapshots:
neo-async@2.6.2: {}
+ next-basics@0.36.0(next@13.5.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ bcryptjs: 2.4.3
+ jsonwebtoken: 9.0.3
+ next: 13.5.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ pure-rand: 6.1.0
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+
next-intl-swc-plugin-extractor@4.9.1: {}
next-intl@4.9.1(@swc/helpers@0.5.21)(next@16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react@19.2.5)(typescript@6.0.3):
@@ -11495,6 +11902,31 @@ snapshots:
transitivePeerDependencies:
- '@swc/helpers'
+ next@13.5.11(@babel/core@7.29.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ dependencies:
+ '@next/env': 13.5.11
+ '@swc/helpers': 0.5.2
+ busboy: 1.6.0
+ caniuse-lite: 1.0.30001788
+ postcss: 8.4.31
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ styled-jsx: 5.1.1(@babel/core@7.29.0)(react@18.3.1)
+ watchpack: 2.4.0
+ optionalDependencies:
+ '@next/swc-darwin-arm64': 13.5.9
+ '@next/swc-darwin-x64': 13.5.9
+ '@next/swc-linux-arm64-gnu': 13.5.9
+ '@next/swc-linux-arm64-musl': 13.5.9
+ '@next/swc-linux-x64-gnu': 13.5.9
+ '@next/swc-linux-x64-musl': 13.5.9
+ '@next/swc-win32-arm64-msvc': 13.5.9
+ '@next/swc-win32-ia32-msvc': 13.5.9
+ '@next/swc-win32-x64-msvc': 13.5.9
+ transitivePeerDependencies:
+ - '@babel/core'
+ - babel-plugin-macros
+
next@16.2.4(@babel/core@7.29.0)(babel-plugin-react-compiler@19.1.0-rc.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5):
dependencies:
'@next/env': 16.2.4
@@ -12359,6 +12791,12 @@ snapshots:
react-stately: 3.46.0(react@19.2.5)
use-sync-external-store: 1.6.0(react@19.2.5)
+ react-dom@18.3.1(react@18.3.1):
+ dependencies:
+ loose-envify: 1.4.0
+ react: 18.3.1
+ scheduler: 0.23.2
+
react-dom@19.2.5(react@19.2.5):
dependencies:
react: 19.2.5
@@ -12372,11 +12810,11 @@ snapshots:
dependencies:
react: 19.2.5
- react-intl@5.25.1(react@18.3.1)(typescript@5.9.3):
+ react-intl@5.25.1(react@18.3.1)(typescript@6.0.3):
dependencies:
'@formatjs/ecma402-abstract': 1.11.4
'@formatjs/icu-messageformat-parser': 2.1.0
- '@formatjs/intl': 2.2.1(typescript@5.9.3)
+ '@formatjs/intl': 2.2.1(typescript@6.0.3)
'@formatjs/intl-displaynames': 5.4.3
'@formatjs/intl-listformat': 6.5.3
'@types/hoist-non-react-statics': 3.3.7(@types/react@18.3.28)
@@ -12386,7 +12824,7 @@ snapshots:
react: 18.3.1
tslib: 2.8.1
optionalDependencies:
- typescript: 5.9.3
+ typescript: 6.0.3
react-is@16.13.1: {}
@@ -12437,6 +12875,10 @@ snapshots:
react: 19.2.5
react-dom: 19.2.5(react@19.2.5)
+ react@18.3.1:
+ dependencies:
+ loose-envify: 1.4.0
+
react@19.2.5: {}
read-cache@1.0.0:
@@ -12892,6 +13334,8 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
+ streamsearch@1.1.0: {}
+
string-hash@1.1.3: {}
string-length@4.0.2:
@@ -12961,12 +13405,19 @@ snapshots:
strip-json-comments@3.1.1: {}
- stripe@20.4.1(@types/node@24.10.13):
+ stripe@20.4.1(@types/node@25.6.0):
optionalDependencies:
- '@types/node': 24.10.13
+ '@types/node': 25.6.0
style-inject@0.3.0: {}
+ styled-jsx@5.1.1(@babel/core@7.29.0)(react@18.3.1):
+ dependencies:
+ client-only: 0.0.1
+ react: 18.3.1
+ optionalDependencies:
+ '@babel/core': 7.29.0
+
styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.5):
dependencies:
client-only: 0.0.1
@@ -13336,6 +13787,10 @@ snapshots:
intl-messageformat: 11.2.1
react: 19.2.5
+ use-sync-external-store@1.6.0(react@18.3.1):
+ dependencies:
+ react: 18.3.1
+
use-sync-external-store@1.6.0(react@19.2.5):
dependencies:
react: 19.2.5
@@ -13499,6 +13954,14 @@ snapshots:
zod@4.3.6: {}
+ zustand@4.5.7(@types/react@18.3.28)(immer@9.0.21)(react@18.3.1):
+ dependencies:
+ use-sync-external-store: 1.6.0(react@18.3.1)
+ optionalDependencies:
+ '@types/react': 18.3.28
+ immer: 9.0.21
+ react: 18.3.1
+
zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)):
optionalDependencies:
'@types/react': 19.2.14
diff --git a/prisma/migrations/20_add_billing/migration.sql b/prisma/migrations/20_add_billing/migration.sql
new file mode 100644
index 000000000..4fd6d7de0
--- /dev/null
+++ b/prisma/migrations/20_add_billing/migration.sql
@@ -0,0 +1,64 @@
+-- CreateTable
+CREATE TABLE "billing_invoice" (
+ "line_id" VARCHAR(255) NOT NULL,
+ "provider_id" UUID NOT NULL,
+ "invoice_id" VARCHAR(255) NOT NULL,
+ "customer_id" VARCHAR(255) NOT NULL,
+ "invoice_status" VARCHAR(50) NOT NULL,
+ "invoice_period_end" TIMESTAMPTZ(6) NOT NULL,
+ "usage_type" VARCHAR(20) NOT NULL,
+ "amount_cents" INTEGER NOT NULL,
+ "period_start" TIMESTAMPTZ(6) NOT NULL,
+ "period_end" TIMESTAMPTZ(6) NOT NULL,
+ "period_months" INTEGER NOT NULL,
+ "mrr_cents" INTEGER NOT NULL,
+
+ CONSTRAINT "billing_invoice_pkey" PRIMARY KEY ("line_id")
+);
+
+-- CreateTable
+CREATE TABLE "billing_provider" (
+ "billing_id" UUID NOT NULL,
+ "name" VARCHAR(100) NOT NULL,
+ "provider" VARCHAR(50) NOT NULL,
+ "user_id" UUID,
+ "team_id" UUID,
+ "api_key" TEXT NOT NULL,
+ "sync_cursor" VARCHAR(255),
+ "sync_status" VARCHAR(20) NOT NULL DEFAULT 'idle',
+ "last_run_at" TIMESTAMPTZ(6),
+ "created_at" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMPTZ(6),
+
+ CONSTRAINT "billing_provider_pkey" PRIMARY KEY ("billing_id")
+);
+
+-- CreateIndex
+CREATE INDEX "billing_invoice_provider_id_idx" ON "billing_invoice"("provider_id");
+
+-- CreateIndex
+CREATE INDEX "billing_invoice_invoice_id_idx" ON "billing_invoice"("invoice_id");
+
+-- CreateIndex
+CREATE INDEX "billing_invoice_customer_id_idx" ON "billing_invoice"("customer_id");
+
+-- CreateIndex
+CREATE INDEX "billing_invoice_invoice_status_idx" ON "billing_invoice"("invoice_status");
+
+-- CreateIndex
+CREATE INDEX "billing_invoice_usage_type_idx" ON "billing_invoice"("usage_type");
+
+-- CreateIndex
+CREATE INDEX "billing_provider_user_id_idx" ON "billing_provider"("user_id");
+
+-- CreateIndex
+CREATE INDEX "billing_provider_team_id_idx" ON "billing_provider"("team_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "billing_provider_name_user_id_key" ON "billing_provider"("name", "user_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "billing_provider_name_team_id_key" ON "billing_provider"("name", "team_id");
+
+-- CreateIndex
+CREATE INDEX "session_replay_visit_id_idx" ON "session_replay"("visit_id");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 127ff2b43..2e4f1a2ab 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -415,7 +415,7 @@ model BillingInvoice {
periodMonths Int @map("period_months") @db.Integer
mrrCents Int @map("mrr_cents") @db.Integer
- billingProvider BillingProvider @relation(fields: [providerId], references: [id])
+ billingProvider Billing @relation(fields: [providerId], references: [id])
@@index([providerId])
@@index([invoiceId])
@@ -425,21 +425,23 @@ model BillingInvoice {
@@map("billing_invoice")
}
-model BillingProvider {
- id String @id @default(uuid()) @map("billing_provider_id") @db.Uuid
- provider String @db.VarChar(50)
- userId String? @map("user_id") @db.Uuid
- teamId String? @map("team_id") @db.Uuid
- apiKey String @map("api_key") @db.Text
- syncCursor String? @map("sync_cursor") @db.VarChar(255)
- syncStatus String @default("idle") @map("sync_status") @db.VarChar(20)
- createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
- updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
+model Billing {
+ id String @id @default(uuid()) @map("billing_id") @db.Uuid
+ name String @db.VarChar(100)
+ provider String @db.VarChar(50)
+ userId String? @map("user_id") @db.Uuid
+ teamId String? @map("team_id") @db.Uuid
+ apiKey String @map("api_key") @db.Text
+ syncCursor String? @map("sync_cursor") @db.VarChar(255)
+ syncStatus String @default("idle") @map("sync_status") @db.VarChar(20)
+ lastRunAt DateTime? @map("last_run_at") @db.Timestamptz(6)
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
+ updatedAt DateTime? @map("updated_at") @db.Timestamptz(6)
invoices BillingInvoice[]
- @@unique([provider, userId])
- @@unique([provider, teamId])
+ @@unique([name, userId])
+ @@unique([name, teamId])
@@index([userId])
@@index([teamId])
@@map("billing_provider")
diff --git a/src/app/(main)/SideNav.tsx b/src/app/(main)/SideNav.tsx
index 2f2c27ec4..1261f28c1 100644
--- a/src/app/(main)/SideNav.tsx
+++ b/src/app/(main)/SideNav.tsx
@@ -10,6 +10,7 @@ import {
LinkIcon,
PanelLeft,
PanelsLeftBottom,
+ BadgeDollarSign
} from '@/components/icons';
import { UserButton } from '@/components/input/UserButton';
import { Logo } from '@/components/svg';
@@ -34,13 +35,13 @@ export function SideNav(props: any) {
const links = [
...(!teamId
? [
- {
- id: 'dashboard',
- label: t(labels.dashboard),
- path: '/dashboard',
- icon: ,
- },
- ]
+ {
+ id: 'dashboard',
+ label: t(labels.dashboard),
+ path: '/dashboard',
+ icon: ,
+ },
+ ]
: []),
{
id: 'boards',
@@ -66,6 +67,12 @@ export function SideNav(props: any) {
path: '/pixels',
icon: ,
},
+ {
+ id: 'billing',
+ label: t(labels.billing),
+ path: '/billing',
+ icon: ,
+ },
];
return (
diff --git a/src/app/(main)/billings/BillingsAddButton.tsx b/src/app/(main)/billings/BillingsAddButton.tsx
new file mode 100644
index 000000000..ebff50244
--- /dev/null
+++ b/src/app/(main)/billings/BillingsAddButton.tsx
@@ -0,0 +1,14 @@
+import { useMessages } from '@/components/hooks';
+import { Plus } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { BillingsEditForm } from './BillingsEditForm';
+
+export function BillingsAddButton() {
+ const { t, labels } = useMessages();
+
+ return (
+ } label={t(labels.addBillingProvider)} variant="primary" width="500px">
+ {({ close }) => }
+
+ );
+}
diff --git a/src/app/(main)/billings/BillingsDataTable.tsx b/src/app/(main)/billings/BillingsDataTable.tsx
new file mode 100644
index 000000000..47fe9c0da
--- /dev/null
+++ b/src/app/(main)/billings/BillingsDataTable.tsx
@@ -0,0 +1,13 @@
+import { DataGrid } from '@/components/common/DataGrid';
+import { useBillingProvidersQuery } from '@/components/hooks';
+import { BillingsTable } from './BillingsTable';
+
+export function BillingsDataTable() {
+ const query = useBillingProvidersQuery();
+
+ return (
+
+ {({ data }) => }
+
+ );
+}
diff --git a/src/app/(main)/billings/BillingsDeleteButton.tsx b/src/app/(main)/billings/BillingsDeleteButton.tsx
new file mode 100644
index 000000000..82362f071
--- /dev/null
+++ b/src/app/(main)/billings/BillingsDeleteButton.tsx
@@ -0,0 +1,48 @@
+import { ConfirmationForm } from '@/components/common/ConfirmationForm';
+import { useDeleteQuery, useMessages } from '@/components/hooks';
+import { Trash } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+
+export function BillingsDeleteButton({
+ providerId,
+ providerName,
+ onSave,
+}: {
+ providerId: string;
+ providerName: string;
+ onSave?: () => void;
+}) {
+ const { t, labels, messages, getErrorMessage } = useMessages();
+ const { mutateAsync, isPending, error, touch } = useDeleteQuery(
+ `/billing/providers/${providerId}`,
+ );
+
+ const handleConfirm = async (close: () => void) => {
+ await mutateAsync(null, {
+ onSuccess: () => {
+ touch('billingProviders');
+ onSave?.();
+ close();
+ },
+ });
+ };
+
+ return (
+ } title={t(labels.confirm)} variant="quiet" width="400px">
+ {({ close }) => (
+ {chunks},
+ })}
+ isLoading={isPending}
+ error={getErrorMessage(error)}
+ onConfirm={handleConfirm.bind(null, close)}
+ onClose={close}
+ buttonLabel={t(labels.delete)}
+ buttonVariant="danger"
+ />
+ )}
+
+ );
+}
diff --git a/src/app/(main)/billings/BillingsEditForm.tsx b/src/app/(main)/billings/BillingsEditForm.tsx
new file mode 100644
index 000000000..8e448beab
--- /dev/null
+++ b/src/app/(main)/billings/BillingsEditForm.tsx
@@ -0,0 +1,87 @@
+import { Box, Button, Form, FormField, FormSubmitButton, ListItem, Row, Select, TextField } from '@umami/react-zen';
+import { useLoginQuery, useMessages, useUpdateQuery } from '@/components/hooks';
+import { BILLING_PROVIDER_TYPES } from '@/lib/constants';
+
+interface BillingsFormValues {
+ name: string;
+ provider: string;
+ apiKey: string;
+}
+
+export function BillingsEditForm({
+ providerId,
+ providerName,
+ displayName,
+ onSave,
+ onClose,
+}: {
+ providerId?: string;
+ providerName?: string;
+ displayName?: string;
+ onSave?: () => void;
+ onClose?: () => void;
+}) {
+ const { t, labels, messages, getErrorMessage } = useMessages();
+ const { user } = useLoginQuery();
+ const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(
+ providerId ? `/billing/providers/${providerId}` : '/billing/providers',
+ providerId ? undefined : { userId: user?.id },
+ );
+
+ const handleSubmit = async (data: BillingsFormValues) => {
+ await mutateAsync({ name: data.name, provider: data.provider, apiKey: data.apiKey || undefined });
+ toast(t(messages.saved));
+ touch('billingProviders');
+ onSave?.();
+ onClose?.();
+ };
+
+ return (
+
+ );
+}
diff --git a/src/app/(main)/billings/BillingsPage.tsx b/src/app/(main)/billings/BillingsPage.tsx
new file mode 100644
index 000000000..7963045fe
--- /dev/null
+++ b/src/app/(main)/billings/BillingsPage.tsx
@@ -0,0 +1,25 @@
+'use client';
+import { Column } from '@umami/react-zen';
+import { PageBody } from '@/components/common/PageBody';
+import { PageHeader } from '@/components/common/PageHeader';
+import { Panel } from '@/components/common/Panel';
+import { useMessages } from '@/components/hooks';
+import { BillingsAddButton } from './BillingsAddButton';
+import { BillingsDataTable } from './BillingsDataTable';
+
+export function BillingsPage() {
+ const { t, labels } = useMessages();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(main)/billings/BillingsTable.tsx b/src/app/(main)/billings/BillingsTable.tsx
new file mode 100644
index 000000000..160666bc2
--- /dev/null
+++ b/src/app/(main)/billings/BillingsTable.tsx
@@ -0,0 +1,34 @@
+import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
+import { DateDistance } from '@/components/common/DateDistance';
+import { useMessages } from '@/components/hooks';
+import { Pencil } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { BillingsDeleteButton } from './BillingsDeleteButton';
+import { BillingsEditForm } from './BillingsEditForm';
+
+export function BillingsTable(props: DataTableProps) {
+ const { t, labels } = useMessages();
+
+ return (
+
+
+
+
+
+ {(row: any) => (row.lastRunAt ? : '—')}
+
+
+ {({ id, name, provider }: any) => (
+
+ } title={t(labels.edit)} variant="quiet" width="500px">
+ {({ close }) => (
+
+ )}
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/app/(main)/billings/[billingId]/BillingsPage.tsx b/src/app/(main)/billings/[billingId]/BillingsPage.tsx
new file mode 100644
index 000000000..9ff3f0b37
--- /dev/null
+++ b/src/app/(main)/billings/[billingId]/BillingsPage.tsx
@@ -0,0 +1,5 @@
+'use client';
+
+export function BillingsPage({ billingId }: { billingId: string }) {
+ return {billingId}
;
+}
diff --git a/src/app/(main)/billings/[billingId]/page.tsx b/src/app/(main)/billings/[billingId]/page.tsx
new file mode 100644
index 000000000..fb1518200
--- /dev/null
+++ b/src/app/(main)/billings/[billingId]/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { BillingsPage } from './BillingsPage';
+
+export default async function ({ params }: { params: Promise<{ billingId: string }> }) {
+ const { billingId } = await params;
+
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Billing',
+};
diff --git a/src/app/(main)/billings/page.tsx b/src/app/(main)/billings/page.tsx
new file mode 100644
index 000000000..2a46c83a9
--- /dev/null
+++ b/src/app/(main)/billings/page.tsx
@@ -0,0 +1,10 @@
+import type { Metadata } from 'next';
+import { BillingsPage } from './BillingsPage';
+
+export default function () {
+ return ;
+}
+
+export const metadata: Metadata = {
+ title: 'Billing',
+};
diff --git a/src/app/api/billing/sync/status/route.ts b/src/app/api/billing/sync/status/route.ts
deleted file mode 100644
index 17591682d..000000000
--- a/src/app/api/billing/sync/status/route.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { NextResponse } from 'next/server';
-import { getBillingProviderSyncStatuses } from '@/queries/prisma';
-
-export async function GET() {
- const keys = await getBillingProviderSyncStatuses();
- return NextResponse.json(keys);
-}
diff --git a/src/app/api/billings/[billingId]/route.ts b/src/app/api/billings/[billingId]/route.ts
new file mode 100644
index 000000000..13c4877f5
--- /dev/null
+++ b/src/app/api/billings/[billingId]/route.ts
@@ -0,0 +1,131 @@
+import { z } from 'zod';
+import { decrypt, encrypt, secret } from '@/lib/crypto';
+import { parseRequest } from '@/lib/request';
+import { json, notFound, ok, unauthorized } from '@/lib/response';
+import { deleteBillingById, getBillingById, maskKey, updateBilling } from '@/queries/prisma';
+
+function canAccess(
+ user: { id: string; isAdmin: boolean },
+ row: { userId: string | null; teamId: string | null },
+) {
+ return user.isAdmin || row.userId === user.id;
+}
+
+export async function GET(
+ request: Request,
+ { params }: { params: Promise<{ providerId: string }> },
+) {
+ const { auth, error } = await parseRequest(request, z.object({}));
+
+ if (error) {
+ return error();
+ }
+
+ if (!auth.user) {
+ return unauthorized();
+ }
+
+ const { providerId } = await params;
+ const row = await getBillingById(providerId);
+
+ if (!row) {
+ return notFound();
+ }
+
+ if (!canAccess(auth.user, row)) {
+ return unauthorized();
+ }
+
+ const rawKey = decrypt(row.apiKey, secret());
+
+ return json({
+ id: row.id,
+ provider: row.provider,
+ userId: row.userId,
+ teamId: row.teamId,
+ keyPreview: maskKey(rawKey),
+ syncStatus: row.syncStatus,
+ syncCursor: row.syncCursor,
+ createdAt: row.createdAt,
+ updatedAt: row.updatedAt,
+ });
+}
+
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ providerId: string }> },
+) {
+ const schema = z.object({
+ name: z.string().max(100).optional(),
+ provider: z.string().max(50).optional(),
+ apiKey: z.string().min(1).optional(),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ if (!auth.user) {
+ return unauthorized();
+ }
+
+ const { providerId } = await params;
+ const existing = await getBillingById(providerId);
+
+ if (!existing) {
+ return notFound();
+ }
+
+ if (!canAccess(auth.user, existing)) {
+ return unauthorized();
+ }
+
+ const update: Record = {};
+ if (body.name) update.name = body.name;
+ if (body.provider) update.provider = body.provider;
+ if (body.apiKey) update.apiKey = encrypt(body.apiKey, secret());
+ const row = await updateBilling(providerId, update);
+ const rawKey = decrypt(row.apiKey, secret());
+
+ return json({
+ id: row.id,
+ provider: row.provider,
+ userId: row.userId,
+ teamId: row.teamId,
+ keyPreview: maskKey(rawKey),
+ syncStatus: row.syncStatus,
+ updatedAt: row.updatedAt,
+ });
+}
+
+export async function DELETE(
+ request: Request,
+ { params }: { params: Promise<{ providerId: string }> },
+) {
+ const { auth, error } = await parseRequest(request, z.object({}));
+
+ if (error) {
+ return error();
+ }
+
+ if (!auth.user) {
+ return unauthorized();
+ }
+
+ const { providerId } = await params;
+ const row = await getBillingById(providerId);
+
+ if (!row) {
+ return notFound();
+ }
+
+ if (!canAccess(auth.user, row)) {
+ return unauthorized();
+ }
+
+ await deleteBillingById(providerId);
+
+ return ok();
+}
diff --git a/src/app/api/billing/sync/invoices/route.ts b/src/app/api/billings/[billingId]/sync/route.ts
similarity index 62%
rename from src/app/api/billing/sync/invoices/route.ts
rename to src/app/api/billings/[billingId]/sync/route.ts
index 1827f480a..c80d24186 100644
--- a/src/app/api/billing/sync/invoices/route.ts
+++ b/src/app/api/billings/[billingId]/sync/route.ts
@@ -1,24 +1,20 @@
-import { NextResponse } from 'next/server';
-import Stripe from 'stripe';
import { decrypt, secret } from '@/lib/crypto';
import { fetchInvoicePageBackfill, fetchInvoicePageIncremental } from '@/lib/stripe';
-import { getBillingProviderById, updateBillingProviderSync } from '@/queries/prisma';
-import { STALE_RUNNING_MS, upsertInvoiceBatch } from '@/queries/prisma/billing';
+import { getBillingById, updateBillingSync } from '@/queries/prisma';
+import { STALE_RUNNING_MS, upsertInvoiceBatch } from '@/queries/prisma/billingInvoice';
+import { NextResponse } from 'next/server';
+import Stripe from 'stripe';
-export async function GET(request: Request) {
- const { searchParams } = new URL(request.url);
+export async function POST(
+ request: Request,
+ { params }: { params: Promise<{ billingId: string }> },
+) {
+ const { billingId } = await params;
+ const { mode: rawMode } = await request.json();
+ const mode = rawMode === 'full' ? 'full' : 'batch';
- // keyId identifies which BillingProvider (i.e. which owner) to sync.
- // mode=full — fetch all pages in one request (self-hosted, no timeout concern)
- // mode=batch — fetch one page and return; caller re-invokes (Vercel / default)
- const keyId = searchParams.get('keyId');
- const mode = searchParams.get('mode') === 'full' ? 'full' : 'batch';
+ const keyRow = await getBillingById(billingId);
- if (!keyId) {
- return NextResponse.json({ error: 'keyId is required' }, { status: 400 });
- }
-
- const keyRow = await getBillingProviderById(keyId);
if (!keyRow) {
return NextResponse.json({ error: 'API key not found' }, { status: 404 });
}
@@ -31,7 +27,7 @@ export async function GET(request: Request) {
return NextResponse.json({ skipped: true, reason: 'already running' }, { status: 409 });
}
- await updateBillingProviderSync(keyId, { syncStatus: 'running' });
+ await updateBillingSync(billingId, { syncStatus: 'running' });
// Decrypt the API key and create a scoped Stripe client
const rawApiKey = decrypt(keyRow.apiKey, secret());
@@ -42,7 +38,6 @@ export async function GET(request: Request) {
let processed = 0;
let lastCursor: string | null = keyRow.syncCursor ?? null;
- // If backfilling, always fetch in batch mode to avoid partial syncs. Otherwise, follow the requested mode.
if (mode === 'full') {
let hasMore = true;
while (hasMore) {
@@ -50,7 +45,7 @@ export async function GET(request: Request) {
? await fetchInvoicePageBackfill(stripe, lastCursor)
: await fetchInvoicePageIncremental(stripe);
- await upsertInvoiceBatch(page.data, keyId);
+ await upsertInvoiceBatch(page.data, billingId);
processed += page.data.length;
hasMore = page.has_more;
@@ -59,7 +54,11 @@ export async function GET(request: Request) {
if (!isBackfilling) break;
}
- await updateBillingProviderSync(keyId, { syncStatus: 'idle', syncCursor: null });
+ await updateBillingSync(billingId, {
+ syncStatus: 'idle',
+ syncCursor: null,
+ lastRunAt: new Date(),
+ });
return NextResponse.json({ processed, hasMore: false, cursor: null, status: 'idle' });
} else {
@@ -67,13 +66,17 @@ export async function GET(request: Request) {
? await fetchInvoicePageBackfill(stripe, lastCursor)
: await fetchInvoicePageIncremental(stripe);
- await upsertInvoiceBatch(page.data, keyId);
+ await upsertInvoiceBatch(page.data, billingId);
const nextCursor =
page.has_more && page.data.length > 0 ? page.data[page.data.length - 1].id : null;
const nextStatus = page.has_more ? 'backfilling' : 'idle';
- await updateBillingProviderSync(keyId, { syncStatus: nextStatus, syncCursor: nextCursor });
+ await updateBillingSync(billingId, {
+ syncStatus: nextStatus,
+ syncCursor: nextCursor,
+ ...(nextStatus === 'idle' && { lastRunAt: new Date() }),
+ });
return NextResponse.json({
processed: page.data.length,
@@ -83,7 +86,7 @@ export async function GET(request: Request) {
});
}
} catch (err) {
- await updateBillingProviderSync(keyId, { syncStatus: 'idle' });
+ await updateBillingSync(billingId, { syncStatus: 'idle' });
throw err;
}
}
diff --git a/src/app/api/billings/route.ts b/src/app/api/billings/route.ts
new file mode 100644
index 000000000..c6ee9d28b
--- /dev/null
+++ b/src/app/api/billings/route.ts
@@ -0,0 +1,108 @@
+import { z } from 'zod';
+import { decrypt, encrypt, secret } from '@/lib/crypto';
+import { parseRequest } from '@/lib/request';
+import { json, unauthorized } from '@/lib/response';
+import { pagingParams, searchParams } from '@/lib/schema';
+import {
+ getBillingsPage,
+ maskKey,
+ upsertBillingForTeam,
+ upsertBillingForUser,
+} from '@/queries/prisma';
+
+export async function GET(request: Request) {
+ const schema = z.object({
+ userId: z.string().uuid().optional(),
+ teamId: z.string().uuid().optional(),
+ ...pagingParams,
+ ...searchParams,
+ });
+
+ const { auth, query, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ if (!auth.user) {
+ return unauthorized();
+ }
+
+ const { userId, teamId, ...filters } = query;
+
+ let where: Record = {};
+
+ if (teamId) {
+ if (!auth.user.isAdmin) {
+ return unauthorized();
+ }
+ where = { teamId };
+ } else {
+ const ownerId = userId ?? auth.user.id;
+ if (!auth.user.isAdmin && ownerId !== auth.user.id) {
+ return unauthorized();
+ }
+ where = auth.user.isAdmin && !userId ? {} : { userId: ownerId };
+ }
+
+ const result = await getBillingsPage(where, filters);
+
+ return json(result);
+}
+
+export async function POST(request: Request) {
+ const schema = z.object({
+ name: z.string().max(100),
+ provider: z.string().max(50),
+ apiKey: z.string().min(1),
+ userId: z.string().uuid().optional(),
+ teamId: z.string().uuid().optional(),
+ });
+
+ const { auth, body, error } = await parseRequest(request, schema);
+
+ if (error) {
+ return error();
+ }
+
+ if (!auth.user) {
+ return unauthorized();
+ }
+
+ const { name, provider, apiKey, userId, teamId } = body;
+
+ if (!userId && !teamId) {
+ return new Response(JSON.stringify({ error: 'userId or teamId is required' }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' },
+ });
+ }
+
+ if (!auth.user.isAdmin) {
+ if (userId && userId !== auth.user.id) {
+ return unauthorized();
+ }
+ if (teamId) {
+ return unauthorized();
+ }
+ }
+
+ const encryptedKey = encrypt(apiKey, secret());
+
+ const row = userId
+ ? await upsertBillingForUser(userId, provider, name, encryptedKey)
+ : await upsertBillingForTeam(teamId, provider, name, encryptedKey);
+
+ const rawKey = decrypt(row.apiKey, secret());
+
+ return json({
+ id: row.id,
+ provider: row.provider,
+ userId: row.userId,
+ teamId: row.teamId,
+ keyPreview: maskKey(rawKey),
+ syncStatus: row.syncStatus,
+ createdAt: row.createdAt,
+ updatedAt: row.updatedAt,
+ });
+}
diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts
index a5dfd589f..1cffec5af 100644
--- a/src/components/hooks/index.ts
+++ b/src/components/hooks/index.ts
@@ -11,6 +11,7 @@ export * from './context/useWebsite';
// Query hooks
export * from './queries/useActiveUsersQuery';
+export * from './queries/useBillingProvidersQuery';
export * from './queries/useBoardQuery';
export * from './queries/useBoardSharesQuery';
export * from './queries/useBoardsQuery';
diff --git a/src/components/hooks/queries/useBillingProvidersQuery.ts b/src/components/hooks/queries/useBillingProvidersQuery.ts
new file mode 100644
index 000000000..dbee7ec51
--- /dev/null
+++ b/src/components/hooks/queries/useBillingProvidersQuery.ts
@@ -0,0 +1,18 @@
+import type { ReactQueryOptions } from '@/lib/types';
+import { useApi } from '../useApi';
+import { useModified } from '../useModified';
+import { usePagedQuery } from '../usePagedQuery';
+
+export function useBillingProvidersQuery(
+ params?: Record,
+ options?: ReactQueryOptions,
+) {
+ const { get } = useApi();
+ const { modified } = useModified('billingProviders');
+
+ return usePagedQuery({
+ queryKey: ['billingProviders', { modified, ...params }],
+ queryFn: pageParams => get('/billing/providers', { ...pageParams, ...params }),
+ ...options,
+ });
+}
diff --git a/src/components/messages.ts b/src/components/messages.ts
index 72ce903cf..416f55671 100644
--- a/src/components/messages.ts
+++ b/src/components/messages.ts
@@ -314,6 +314,14 @@ export const labels: Record = {
paidVideo: 'label.paid-video',
grouped: 'label.grouped',
other: 'label.other',
+ billings: 'label.billings',
+ billing: 'label.billing',
+ addBillingProvider: 'label.add-billing-provider',
+ provider: 'label.provider',
+ apiKey: 'label.api-key',
+ syncStatus: 'label.sync-status',
+ syncCursor: 'label.sync-cursor',
+ lastRun: 'label.last-run',
boards: 'label.boards',
apply: 'label.apply',
match: 'label.match',
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index f7c182b15..23d47def8 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -216,6 +216,10 @@ export const ROLE_PERMISSIONS = {
[ROLES.teamViewOnly]: [],
} as const;
+export const BILLING_PROVIDER_TYPES = {
+ stripe: 'stripe',
+};
+
export const THEME_COLORS = {
light: {
primary: '#2680eb',
diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts
index 1da0f81b7..f3e3f0758 100644
--- a/src/lib/prisma.ts
+++ b/src/lib/prisma.ts
@@ -475,9 +475,9 @@ function getSearchParameters(query: string, filters: Record[]) {
[key]:
typeof value === 'string'
? {
- [value]: query,
- mode: 'insensitive',
- }
+ [value]: query,
+ mode: 'insensitive',
+ }
: parseFilter(value),
};
};
@@ -518,7 +518,10 @@ function getClient() {
const schema = getSchema();
- const baseAdapter = new PrismaPg({ connectionString: url }, { schema });
+ console.log(schema, replicaUrl, url, 'replica adapter');
+
+
+ const baseAdapter = new PrismaPg({ connectionString: url }, schema ? { schema } : {});
const baseClient = new PrismaClient({
adapter: baseAdapter,
@@ -536,7 +539,7 @@ function getClient() {
return baseClient;
}
- const replicaAdapter = new PrismaPg({ connectionString: replicaUrl }, { schema });
+ const replicaAdapter = new PrismaPg({ connectionString: replicaUrl }, schema ? { schema } : {});
const replicaClient = new PrismaClient({
adapter: replicaAdapter,
diff --git a/src/queries/prisma/billing.ts b/src/queries/prisma/billing.ts
index a6b5b0cea..f69779b01 100644
--- a/src/queries/prisma/billing.ts
+++ b/src/queries/prisma/billing.ts
@@ -1,62 +1,121 @@
-import type Stripe from 'stripe';
+import { uuid } from '@/lib/crypto';
import prisma from '@/lib/prisma';
-export const STALE_RUNNING_MS = 2 * 60 * 1000;
+const db = () => (prisma.client as any).billing;
-// Upsert a batch of invoice line items into billing_invoice.
-export async function upsertInvoiceBatch(
- invoices: Stripe.Invoice[],
- providerId: string,
-): Promise {
- const db = prisma.client as any;
-
- for (const invoice of invoices) {
- const customerId = invoice.customer as string;
- const invoiceStatus = invoice.status ?? 'unknown';
- const invoicePeriodEnd = new Date(invoice.period_end * 1000);
-
- for (const line of (invoice.lines as any).data) {
- const usageType = line.pricing?.price_details?.price?.recurring?.usage_type;
- const lineType: string | null =
- usageType === 'licensed'
- ? 'licensed'
- : usageType === 'metered'
- ? 'metered'
- : usageType == null && line.amount != null
- ? 'one_time'
- : null;
-
- if (!lineType) continue;
-
- const periodMonths = Math.max(
- 1,
- Math.round((line.period.end - line.period.start) / (86400 * 30.44)),
- );
-
- await db.billingInvoice.upsert({
- where: { id: line.id },
- create: {
- id: line.id,
- providerId,
- invoiceId: invoice.id,
- customerId,
- invoiceStatus,
- invoicePeriodEnd,
- usageType: lineType,
- amountCents: line.amount,
- periodStart: new Date(line.period.start * 1000),
- periodEnd: new Date(line.period.end * 1000),
- periodMonths,
- mrrCents: Math.round(line.amount / periodMonths),
- },
- update: {
- invoiceStatus,
- invoicePeriodEnd,
- amountCents: line.amount,
- usageType: lineType,
- mrrCents: Math.round(line.amount / periodMonths),
- },
- });
- }
- }
+function maskKey(apiKey: string): string {
+ // Show last 4 chars: sk_live_****abcd
+ return apiKey.length > 4 ? `****${apiKey.slice(-4)}` : '****';
}
+
+export async function getBillingByUser(userId: string, provider: string) {
+ return db().findUnique({
+ where: { provider_userId: { provider, userId } },
+ });
+}
+
+export async function getBillingByTeam(teamId: string, provider: string) {
+ return db().findUnique({
+ where: { provider_teamId: { provider, teamId } },
+ });
+}
+
+export async function upsertBillingForUser(
+ userId: string,
+ provider: string,
+ name: string,
+ encryptedKey: string,
+) {
+ return db().upsert({
+ where: { provider_userId: { provider, userId } },
+ create: { id: uuid(), name, provider, userId, apiKey: encryptedKey, updatedAt: new Date() },
+ update: { name, apiKey: encryptedKey, updatedAt: new Date() },
+ });
+}
+
+export async function upsertBillingForTeam(
+ teamId: string,
+ provider: string,
+ name: string,
+ encryptedKey: string,
+) {
+ return db().upsert({
+ where: { provider_teamId: { provider, teamId } },
+ create: { id: uuid(), name, provider, teamId, apiKey: encryptedKey, updatedAt: new Date() },
+ update: { name, apiKey: encryptedKey, updatedAt: new Date() },
+ });
+}
+
+export async function deleteBillingByUser(userId: string, provider: string) {
+ return db().delete({
+ where: { provider_userId: { provider, userId } },
+ });
+}
+
+export async function deleteBillingByTeam(teamId: string, provider: string) {
+ return db().delete({
+ where: { provider_teamId: { provider, teamId } },
+ });
+}
+
+export async function getBillingById(id: string) {
+ return db().findUnique({ where: { id } });
+}
+
+export async function updateBillingSync(
+ id: string,
+ data: { syncStatus: string; syncCursor?: string | null; lastRunAt?: Date | null },
+) {
+ return db().update({ where: { id }, data });
+}
+
+export async function getBillingSyncStatuses() {
+ return db().findMany({
+ select: {
+ id: true,
+ provider: true,
+ userId: true,
+ teamId: true,
+ syncStatus: true,
+ syncCursor: true,
+ updatedAt: true,
+ },
+ });
+}
+
+const providerSelect = {
+ id: true,
+ name: true,
+ provider: true,
+ userId: true,
+ teamId: true,
+ syncStatus: true,
+ syncCursor: true,
+ lastRunAt: true,
+ createdAt: true,
+ updatedAt: true,
+};
+
+export async function getBillingsPage(
+ where: Record = {},
+ filters?: Record,
+) {
+ return prisma.pagedQuery(
+ 'billing',
+ { where, select: providerSelect },
+ { orderBy: 'createdAt', sortDescending: true, ...filters },
+ );
+}
+
+export async function updateBilling(
+ id: string,
+ data: { name?: string; apiKey?: string; provider?: string },
+) {
+ return db().update({ where: { id }, data: { ...data, updatedAt: new Date() } });
+}
+
+export async function deleteBillingById(id: string) {
+ return db().delete({ where: { id } });
+}
+
+export { maskKey };
diff --git a/src/queries/prisma/billingInvoice.ts b/src/queries/prisma/billingInvoice.ts
new file mode 100644
index 000000000..a6b5b0cea
--- /dev/null
+++ b/src/queries/prisma/billingInvoice.ts
@@ -0,0 +1,62 @@
+import type Stripe from 'stripe';
+import prisma from '@/lib/prisma';
+
+export const STALE_RUNNING_MS = 2 * 60 * 1000;
+
+// Upsert a batch of invoice line items into billing_invoice.
+export async function upsertInvoiceBatch(
+ invoices: Stripe.Invoice[],
+ providerId: string,
+): Promise {
+ const db = prisma.client as any;
+
+ for (const invoice of invoices) {
+ const customerId = invoice.customer as string;
+ const invoiceStatus = invoice.status ?? 'unknown';
+ const invoicePeriodEnd = new Date(invoice.period_end * 1000);
+
+ for (const line of (invoice.lines as any).data) {
+ const usageType = line.pricing?.price_details?.price?.recurring?.usage_type;
+ const lineType: string | null =
+ usageType === 'licensed'
+ ? 'licensed'
+ : usageType === 'metered'
+ ? 'metered'
+ : usageType == null && line.amount != null
+ ? 'one_time'
+ : null;
+
+ if (!lineType) continue;
+
+ const periodMonths = Math.max(
+ 1,
+ Math.round((line.period.end - line.period.start) / (86400 * 30.44)),
+ );
+
+ await db.billingInvoice.upsert({
+ where: { id: line.id },
+ create: {
+ id: line.id,
+ providerId,
+ invoiceId: invoice.id,
+ customerId,
+ invoiceStatus,
+ invoicePeriodEnd,
+ usageType: lineType,
+ amountCents: line.amount,
+ periodStart: new Date(line.period.start * 1000),
+ periodEnd: new Date(line.period.end * 1000),
+ periodMonths,
+ mrrCents: Math.round(line.amount / periodMonths),
+ },
+ update: {
+ invoiceStatus,
+ invoicePeriodEnd,
+ amountCents: line.amount,
+ usageType: lineType,
+ mrrCents: Math.round(line.amount / periodMonths),
+ },
+ });
+ }
+ }
+}
diff --git a/src/queries/prisma/billingProvider.ts b/src/queries/prisma/billingProvider.ts
deleted file mode 100644
index 7d9ad9ec6..000000000
--- a/src/queries/prisma/billingProvider.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-import { uuid } from '@/lib/crypto';
-import prisma from '@/lib/prisma';
-
-const db = () => (prisma.client as any).billingProvider;
-
-function maskKey(apiKey: string): string {
- // Show last 4 chars: sk_live_****abcd
- return apiKey.length > 4 ? `****${apiKey.slice(-4)}` : '****';
-}
-
-export async function getBillingProviderByUser(userId: string, provider: string) {
- return db().findUnique({
- where: { provider_userId: { provider, userId } },
- });
-}
-
-export async function getBillingProviderByTeam(teamId: string, provider: string) {
- return db().findUnique({
- where: { provider_teamId: { provider, teamId } },
- });
-}
-
-export async function upsertBillingProviderForUser(
- userId: string,
- provider: string,
- encryptedKey: string,
-) {
- return db().upsert({
- where: { provider_userId: { provider, userId } },
- create: { id: uuid(), provider, userId, apiKey: encryptedKey },
- update: { apiKey: encryptedKey },
- });
-}
-
-export async function upsertBillingProviderForTeam(
- teamId: string,
- provider: string,
- encryptedKey: string,
-) {
- return db().upsert({
- where: { provider_teamId: { provider, teamId } },
- create: { id: uuid(), provider, teamId, apiKey: encryptedKey },
- update: { apiKey: encryptedKey },
- });
-}
-
-export async function deleteBillingProviderByUser(userId: string, provider: string) {
- return db().delete({
- where: { provider_userId: { provider, userId } },
- });
-}
-
-export async function deleteBillingProviderByTeam(teamId: string, provider: string) {
- return db().delete({
- where: { provider_teamId: { provider, teamId } },
- });
-}
-
-export async function getBillingProviderById(id: string) {
- return db().findUnique({ where: { id } });
-}
-
-export async function updateBillingProviderSync(
- id: string,
- data: { syncStatus: string; syncCursor?: string | null },
-) {
- return db().update({ where: { id }, data });
-}
-
-export async function getBillingProviderSyncStatuses() {
- return db().findMany({
- select: {
- id: true,
- provider: true,
- userId: true,
- teamId: true,
- syncStatus: true,
- syncCursor: true,
- updatedAt: true,
- },
- });
-}
-
-export { maskKey };
diff --git a/src/queries/prisma/index.ts b/src/queries/prisma/index.ts
index ecaffdd5a..f8916fa84 100644
--- a/src/queries/prisma/index.ts
+++ b/src/queries/prisma/index.ts
@@ -1,4 +1,4 @@
-export * from './billingProvider';
+export * from './billing';
export * from './board';
export * from './link';
export * from './pixel';