mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
new selfhosted version
This commit is contained in:
3
payments/.gitignore
vendored
Normal file
3
payments/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
src/shared
|
||||
dist
|
||||
31
payments/package.json
Normal file
31
payments/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "payments",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"dev:prod": "ts-node ../scripts/payments/run_local.ts --production",
|
||||
"dev:test": "ts-node ../scripts/payments/run_local.ts --testmode",
|
||||
"shared": "ts-node ../scripts/payments/shared.ts",
|
||||
"deploy": "ts-node ../scripts/payments/deploy.ts",
|
||||
"build": "pnpm run shared && tsc"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "Emily",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@trpc/client": "^11.4.3",
|
||||
"@trpc/server": "11.2.0",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^4.3.1",
|
||||
"express": "^5.1.0",
|
||||
"mongoose": "^8.15.0",
|
||||
"stripe": "^18.2.1",
|
||||
"zod": "3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.18",
|
||||
"@types/express": "^5.0.2",
|
||||
"@types/node": "^22.15.19"
|
||||
}
|
||||
}
|
||||
930
payments/pnpm-lock.yaml
generated
Normal file
930
payments/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,930 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@trpc/client':
|
||||
specifier: ^11.4.3
|
||||
version: 11.4.3(@trpc/server@11.2.0(typescript@5.8.3))(typescript@5.8.3)
|
||||
'@trpc/server':
|
||||
specifier: 11.2.0
|
||||
version: 11.2.0(typescript@5.8.3)
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.5
|
||||
cron:
|
||||
specifier: ^4.3.1
|
||||
version: 4.3.1
|
||||
express:
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0
|
||||
mongoose:
|
||||
specifier: ^8.15.0
|
||||
version: 8.15.0
|
||||
stripe:
|
||||
specifier: ^18.2.1
|
||||
version: 18.2.1(@types/node@22.15.19)
|
||||
zod:
|
||||
specifier: 3.24.2
|
||||
version: 3.24.2
|
||||
devDependencies:
|
||||
'@types/cors':
|
||||
specifier: ^2.8.18
|
||||
version: 2.8.18
|
||||
'@types/express':
|
||||
specifier: ^5.0.2
|
||||
version: 5.0.2
|
||||
'@types/node':
|
||||
specifier: ^22.15.19
|
||||
version: 22.15.19
|
||||
|
||||
packages:
|
||||
|
||||
'@mongodb-js/saslprep@1.2.2':
|
||||
resolution: {integrity: sha512-EB0O3SCSNRUFk66iRCpI+cXzIjdswfCs7F6nOC3RAGJ7xr5YhaicvsRwJ9eyzYvYRlCSDUO/c7g4yNulxKC1WA==}
|
||||
|
||||
'@trpc/client@11.4.3':
|
||||
resolution: {integrity: sha512-i2suttUCfColktXT8bqex5kHW5jpT15nwUh0hGSDiW1keN621kSUQKcLJ095blqQAUgB+lsmgSqSMmB4L9shQQ==}
|
||||
peerDependencies:
|
||||
'@trpc/server': 11.4.3
|
||||
typescript: '>=5.7.2'
|
||||
|
||||
'@trpc/server@11.2.0':
|
||||
resolution: {integrity: sha512-clESXvCT3rTRUavB3wjtmPDlbB+rayVPeaTeXEQVeQNdTrK5o6GnYfCdiJJSdQspUQAwkGgQFRnku5pckuISlw==}
|
||||
peerDependencies:
|
||||
typescript: '>=5.7.2'
|
||||
|
||||
'@types/body-parser@1.19.5':
|
||||
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
|
||||
|
||||
'@types/cors@2.8.18':
|
||||
resolution: {integrity: sha512-nX3d0sxJW41CqQvfOzVG1NCTXfFDrDWIghCZncpHeWlVFd81zxB/DLhg7avFg6eHLCRX7ckBmoIIcqa++upvJA==}
|
||||
|
||||
'@types/express-serve-static-core@5.0.6':
|
||||
resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==}
|
||||
|
||||
'@types/express@5.0.2':
|
||||
resolution: {integrity: sha512-BtjL3ZwbCQriyb0DGw+Rt12qAXPiBTPs815lsUvtt1Grk0vLRMZNMUZ741d5rjk+UQOxfDiBZ3dxpX00vSkK3g==}
|
||||
|
||||
'@types/http-errors@2.0.4':
|
||||
resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==}
|
||||
|
||||
'@types/luxon@3.6.2':
|
||||
resolution: {integrity: sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==}
|
||||
|
||||
'@types/mime@1.3.5':
|
||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||
|
||||
'@types/node@22.15.19':
|
||||
resolution: {integrity: sha512-3vMNr4TzNQyjHcRZadojpRaD9Ofr6LsonZAoQ+HMUa/9ORTPoxVIw0e0mpqWpdjj8xybyCM+oKOUH2vwFu/oEw==}
|
||||
|
||||
'@types/qs@6.14.0':
|
||||
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
|
||||
|
||||
'@types/range-parser@1.2.7':
|
||||
resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==}
|
||||
|
||||
'@types/send@0.17.4':
|
||||
resolution: {integrity: sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==}
|
||||
|
||||
'@types/serve-static@1.15.7':
|
||||
resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==}
|
||||
|
||||
'@types/webidl-conversions@7.0.3':
|
||||
resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==}
|
||||
|
||||
'@types/whatwg-url@11.0.5':
|
||||
resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==}
|
||||
|
||||
accepts@2.0.0:
|
||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
body-parser@2.2.0:
|
||||
resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
bson@6.10.3:
|
||||
resolution: {integrity: sha512-MTxGsqgYTwfshYWTRdmZRC+M7FnG1b4y7RO7p2k3X24Wq0yv1m77Wsj0BzlPzd/IowgESfsruQCUToa7vbOpPQ==}
|
||||
engines: {node: '>=16.20.1'}
|
||||
|
||||
bytes@3.1.2:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
call-bound@1.0.4:
|
||||
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
content-disposition@1.0.0:
|
||||
resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
content-type@1.0.5:
|
||||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
cookie-signature@1.2.2:
|
||||
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
|
||||
engines: {node: '>=6.6.0'}
|
||||
|
||||
cookie@0.7.2:
|
||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
cors@2.8.5:
|
||||
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
cron@4.3.1:
|
||||
resolution: {integrity: sha512-7x7DoEOxV11t3OPWWMjj1xrL1PGkTV5RV+/54IJTZD7gStiaMploY43EkeBSkDZTLRbUwk+OISbQ0TR133oXyA==}
|
||||
engines: {node: '>=18.x'}
|
||||
|
||||
debug@4.4.1:
|
||||
resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
peerDependenciesMeta:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
depd@2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
ee-first@1.1.1:
|
||||
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
|
||||
|
||||
encodeurl@2.0.0:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
es-define-property@1.0.1:
|
||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-errors@1.3.0:
|
||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
escape-html@1.0.3:
|
||||
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
|
||||
|
||||
etag@1.8.1:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
express@5.1.0:
|
||||
resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
finalhandler@2.1.0:
|
||||
resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
fresh@2.0.0:
|
||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-proto@1.0.1:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-symbols@1.1.0:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
http-errors@2.0.0:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
is-promise@4.0.0:
|
||||
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
|
||||
|
||||
kareem@2.6.3:
|
||||
resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
luxon@3.6.1:
|
||||
resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
media-typer@1.1.0:
|
||||
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
memory-pager@1.5.0:
|
||||
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
|
||||
|
||||
merge-descriptors@2.0.0:
|
||||
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
mime-db@1.54.0:
|
||||
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@3.0.1:
|
||||
resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mongodb-connection-string-url@3.0.2:
|
||||
resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==}
|
||||
|
||||
mongodb@6.16.0:
|
||||
resolution: {integrity: sha512-D1PNcdT0y4Grhou5Zi/qgipZOYeWrhLEpk33n3nm6LGtz61jvO88WlrWCK/bigMjpnOdAUKKQwsGIl0NtWMyYw==}
|
||||
engines: {node: '>=16.20.1'}
|
||||
peerDependencies:
|
||||
'@aws-sdk/credential-providers': ^3.188.0
|
||||
'@mongodb-js/zstd': ^1.1.0 || ^2.0.0
|
||||
gcp-metadata: ^5.2.0
|
||||
kerberos: ^2.0.1
|
||||
mongodb-client-encryption: '>=6.0.0 <7'
|
||||
snappy: ^7.2.2
|
||||
socks: ^2.7.1
|
||||
peerDependenciesMeta:
|
||||
'@aws-sdk/credential-providers':
|
||||
optional: true
|
||||
'@mongodb-js/zstd':
|
||||
optional: true
|
||||
gcp-metadata:
|
||||
optional: true
|
||||
kerberos:
|
||||
optional: true
|
||||
mongodb-client-encryption:
|
||||
optional: true
|
||||
snappy:
|
||||
optional: true
|
||||
socks:
|
||||
optional: true
|
||||
|
||||
mongoose@8.15.0:
|
||||
resolution: {integrity: sha512-WFKsY1q12ScGabnZWUB9c/QzZmz/ESorrV27OembB7Gz6rrh9m3GA4Srsv1uvW1s9AHO5DeZ6DdUTyF9zyNERQ==}
|
||||
engines: {node: '>=16.20.1'}
|
||||
|
||||
mpath@0.9.0:
|
||||
resolution: {integrity: sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
mquery@5.0.0:
|
||||
resolution: {integrity: sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
negotiator@1.0.0:
|
||||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
object-inspect@1.13.4:
|
||||
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
on-finished@2.4.1:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
path-to-regexp@8.2.0:
|
||||
resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
||||
punycode@2.3.1:
|
||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
qs@6.14.0:
|
||||
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
range-parser@1.2.1:
|
||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
raw-body@3.0.0:
|
||||
resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
router@2.2.0:
|
||||
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
send@1.2.0:
|
||||
resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
serve-static@2.2.0:
|
||||
resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
setprototypeof@1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
|
||||
side-channel-list@1.0.0:
|
||||
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel-map@1.0.1:
|
||||
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel-weakmap@1.0.2:
|
||||
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
side-channel@1.1.0:
|
||||
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
sift@17.1.3:
|
||||
resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==}
|
||||
|
||||
sparse-bitfield@3.0.3:
|
||||
resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
|
||||
|
||||
statuses@2.0.1:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
stripe@18.2.1:
|
||||
resolution: {integrity: sha512-GwB1B7WSwEBzW4dilgyJruUYhbGMscrwuyHsPUmSRKrGHZ5poSh2oU9XKdii5BFVJzXHn35geRvGJ6R8bYcp8w==}
|
||||
engines: {node: '>=12.*'}
|
||||
peerDependencies:
|
||||
'@types/node': '>=12.x.x'
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
toidentifier@1.0.1:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
tr46@5.1.1:
|
||||
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
type-is@2.0.1:
|
||||
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
typescript@5.8.3:
|
||||
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
unpipe@1.0.0:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
vary@1.1.2:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
whatwg-url@14.2.0:
|
||||
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
wrappy@1.0.2:
|
||||
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
|
||||
|
||||
zod@3.24.2:
|
||||
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@mongodb-js/saslprep@1.2.2':
|
||||
dependencies:
|
||||
sparse-bitfield: 3.0.3
|
||||
|
||||
'@trpc/client@11.4.3(@trpc/server@11.2.0(typescript@5.8.3))(typescript@5.8.3)':
|
||||
dependencies:
|
||||
'@trpc/server': 11.2.0(typescript@5.8.3)
|
||||
typescript: 5.8.3
|
||||
|
||||
'@trpc/server@11.2.0(typescript@5.8.3)':
|
||||
dependencies:
|
||||
typescript: 5.8.3
|
||||
|
||||
'@types/body-parser@1.19.5':
|
||||
dependencies:
|
||||
'@types/connect': 3.4.38
|
||||
'@types/node': 22.15.19
|
||||
|
||||
'@types/connect@3.4.38':
|
||||
dependencies:
|
||||
'@types/node': 22.15.19
|
||||
|
||||
'@types/cors@2.8.18':
|
||||
dependencies:
|
||||
'@types/node': 22.15.19
|
||||
|
||||
'@types/express-serve-static-core@5.0.6':
|
||||
dependencies:
|
||||
'@types/node': 22.15.19
|
||||
'@types/qs': 6.14.0
|
||||
'@types/range-parser': 1.2.7
|
||||
'@types/send': 0.17.4
|
||||
|
||||
'@types/express@5.0.2':
|
||||
dependencies:
|
||||
'@types/body-parser': 1.19.5
|
||||
'@types/express-serve-static-core': 5.0.6
|
||||
'@types/serve-static': 1.15.7
|
||||
|
||||
'@types/http-errors@2.0.4': {}
|
||||
|
||||
'@types/luxon@3.6.2': {}
|
||||
|
||||
'@types/mime@1.3.5': {}
|
||||
|
||||
'@types/node@22.15.19':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/qs@6.14.0': {}
|
||||
|
||||
'@types/range-parser@1.2.7': {}
|
||||
|
||||
'@types/send@0.17.4':
|
||||
dependencies:
|
||||
'@types/mime': 1.3.5
|
||||
'@types/node': 22.15.19
|
||||
|
||||
'@types/serve-static@1.15.7':
|
||||
dependencies:
|
||||
'@types/http-errors': 2.0.4
|
||||
'@types/node': 22.15.19
|
||||
'@types/send': 0.17.4
|
||||
|
||||
'@types/webidl-conversions@7.0.3': {}
|
||||
|
||||
'@types/whatwg-url@11.0.5':
|
||||
dependencies:
|
||||
'@types/webidl-conversions': 7.0.3
|
||||
|
||||
accepts@2.0.0:
|
||||
dependencies:
|
||||
mime-types: 3.0.1
|
||||
negotiator: 1.0.0
|
||||
|
||||
body-parser@2.2.0:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
content-type: 1.0.5
|
||||
debug: 4.4.1
|
||||
http-errors: 2.0.0
|
||||
iconv-lite: 0.6.3
|
||||
on-finished: 2.4.1
|
||||
qs: 6.14.0
|
||||
raw-body: 3.0.0
|
||||
type-is: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
bson@6.10.3: {}
|
||||
|
||||
bytes@3.1.2: {}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
call-bound@1.0.4:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
get-intrinsic: 1.3.0
|
||||
|
||||
content-disposition@1.0.0:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
content-type@1.0.5: {}
|
||||
|
||||
cookie-signature@1.2.2: {}
|
||||
|
||||
cookie@0.7.2: {}
|
||||
|
||||
cors@2.8.5:
|
||||
dependencies:
|
||||
object-assign: 4.1.1
|
||||
vary: 1.1.2
|
||||
|
||||
cron@4.3.1:
|
||||
dependencies:
|
||||
'@types/luxon': 3.6.2
|
||||
luxon: 3.6.1
|
||||
|
||||
debug@4.4.1:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
ee-first@1.1.1: {}
|
||||
|
||||
encodeurl@2.0.0: {}
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
|
||||
es-errors@1.3.0: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
escape-html@1.0.3: {}
|
||||
|
||||
etag@1.8.1: {}
|
||||
|
||||
express@5.1.0:
|
||||
dependencies:
|
||||
accepts: 2.0.0
|
||||
body-parser: 2.2.0
|
||||
content-disposition: 1.0.0
|
||||
content-type: 1.0.5
|
||||
cookie: 0.7.2
|
||||
cookie-signature: 1.2.2
|
||||
debug: 4.4.1
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
finalhandler: 2.1.0
|
||||
fresh: 2.0.0
|
||||
http-errors: 2.0.0
|
||||
merge-descriptors: 2.0.0
|
||||
mime-types: 3.0.1
|
||||
on-finished: 2.4.1
|
||||
once: 1.4.0
|
||||
parseurl: 1.3.3
|
||||
proxy-addr: 2.0.7
|
||||
qs: 6.14.0
|
||||
range-parser: 1.2.1
|
||||
router: 2.2.0
|
||||
send: 1.2.0
|
||||
serve-static: 2.2.0
|
||||
statuses: 2.0.1
|
||||
type-is: 2.0.1
|
||||
vary: 1.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
finalhandler@2.1.0:
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
on-finished: 2.4.1
|
||||
parseurl: 1.3.3
|
||||
statuses: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
fresh@2.0.0: {}
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
function-bind: 1.1.2
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
hasown: 2.0.2
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
get-proto@1.0.1:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
http-errors@2.0.0:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
inherits: 2.0.4
|
||||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.1
|
||||
toidentifier: 1.0.1
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
ipaddr.js@1.9.1: {}
|
||||
|
||||
is-promise@4.0.0: {}
|
||||
|
||||
kareem@2.6.3: {}
|
||||
|
||||
luxon@3.6.1: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
media-typer@1.1.0: {}
|
||||
|
||||
memory-pager@1.5.0: {}
|
||||
|
||||
merge-descriptors@2.0.0: {}
|
||||
|
||||
mime-db@1.54.0: {}
|
||||
|
||||
mime-types@3.0.1:
|
||||
dependencies:
|
||||
mime-db: 1.54.0
|
||||
|
||||
mongodb-connection-string-url@3.0.2:
|
||||
dependencies:
|
||||
'@types/whatwg-url': 11.0.5
|
||||
whatwg-url: 14.2.0
|
||||
|
||||
mongodb@6.16.0:
|
||||
dependencies:
|
||||
'@mongodb-js/saslprep': 1.2.2
|
||||
bson: 6.10.3
|
||||
mongodb-connection-string-url: 3.0.2
|
||||
|
||||
mongoose@8.15.0:
|
||||
dependencies:
|
||||
bson: 6.10.3
|
||||
kareem: 2.6.3
|
||||
mongodb: 6.16.0
|
||||
mpath: 0.9.0
|
||||
mquery: 5.0.0
|
||||
ms: 2.1.3
|
||||
sift: 17.1.3
|
||||
transitivePeerDependencies:
|
||||
- '@aws-sdk/credential-providers'
|
||||
- '@mongodb-js/zstd'
|
||||
- gcp-metadata
|
||||
- kerberos
|
||||
- mongodb-client-encryption
|
||||
- snappy
|
||||
- socks
|
||||
- supports-color
|
||||
|
||||
mpath@0.9.0: {}
|
||||
|
||||
mquery@5.0.0:
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-inspect@1.13.4: {}
|
||||
|
||||
on-finished@2.4.1:
|
||||
dependencies:
|
||||
ee-first: 1.1.1
|
||||
|
||||
once@1.4.0:
|
||||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-to-regexp@8.2.0: {}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
ipaddr.js: 1.9.1
|
||||
|
||||
punycode@2.3.1: {}
|
||||
|
||||
qs@6.14.0:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
range-parser@1.2.1: {}
|
||||
|
||||
raw-body@3.0.0:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
http-errors: 2.0.0
|
||||
iconv-lite: 0.6.3
|
||||
unpipe: 1.0.0
|
||||
|
||||
router@2.2.0:
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
depd: 2.0.0
|
||||
is-promise: 4.0.0
|
||||
parseurl: 1.3.3
|
||||
path-to-regexp: 8.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
send@1.2.0:
|
||||
dependencies:
|
||||
debug: 4.4.1
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
fresh: 2.0.0
|
||||
http-errors: 2.0.0
|
||||
mime-types: 3.0.1
|
||||
ms: 2.1.3
|
||||
on-finished: 2.4.1
|
||||
range-parser: 1.2.1
|
||||
statuses: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
serve-static@2.2.0:
|
||||
dependencies:
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 1.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
side-channel-list@1.0.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
|
||||
side-channel-map@1.0.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
|
||||
side-channel-weakmap@1.0.2:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
side-channel-map: 1.0.1
|
||||
|
||||
side-channel@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
object-inspect: 1.13.4
|
||||
side-channel-list: 1.0.0
|
||||
side-channel-map: 1.0.1
|
||||
side-channel-weakmap: 1.0.2
|
||||
|
||||
sift@17.1.3: {}
|
||||
|
||||
sparse-bitfield@3.0.3:
|
||||
dependencies:
|
||||
memory-pager: 1.5.0
|
||||
|
||||
statuses@2.0.1: {}
|
||||
|
||||
stripe@18.2.1(@types/node@22.15.19):
|
||||
dependencies:
|
||||
qs: 6.14.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.19
|
||||
|
||||
toidentifier@1.0.1: {}
|
||||
|
||||
tr46@5.1.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
type-is@2.0.1:
|
||||
dependencies:
|
||||
content-type: 1.0.5
|
||||
media-typer: 1.1.0
|
||||
mime-types: 3.0.1
|
||||
|
||||
typescript@5.8.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
|
||||
whatwg-url@14.2.0:
|
||||
dependencies:
|
||||
tr46: 5.1.1
|
||||
webidl-conversions: 7.0.0
|
||||
|
||||
wrappy@1.0.2: {}
|
||||
|
||||
zod@3.24.2: {}
|
||||
98
payments/src/controllers/CustomerController.ts
Normal file
98
payments/src/controllers/CustomerController.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
|
||||
import { PremiumModel } from '../shared/schema/PremiumSchema';
|
||||
import StripeService from '../services/StripeService';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import z from 'zod';
|
||||
|
||||
export const ZGetCustomerInput = z.object({
|
||||
user_id: z.string()
|
||||
})
|
||||
|
||||
export type TGetCustomerInput = z.infer<typeof ZGetCustomerInput>;
|
||||
|
||||
export async function getCustomer(data: TGetCustomerInput) {
|
||||
const premiumData = await PremiumModel.findOne({ user_id: data.user_id });
|
||||
if (!premiumData) throw new TRPCError({ code: 'NOT_FOUND', message: 'Cannot find user. Please contact support.' });
|
||||
if (!premiumData.customer_id) throw new TRPCError({ code: 'NOT_FOUND', message: 'Cannot find user customer. Please contact support.' });
|
||||
const customer = await StripeService.getCustomer(premiumData.customer_id);
|
||||
if (!customer) throw new TRPCError({ code: 'NOT_FOUND', message: 'Cannot find customer. Please contact support.' });
|
||||
if (customer.deleted === true) throw new TRPCError({ code: 'NOT_FOUND', message: 'Customer is deleted. Please contact support.' });
|
||||
return customer.address;
|
||||
}
|
||||
|
||||
export const ZUpdateCustomerInput = z.object({
|
||||
user_id: z.string(),
|
||||
address: z.object({
|
||||
line1: z.string(),
|
||||
line2: z.string(),
|
||||
country: z.string(),
|
||||
postal_code: z.string(),
|
||||
city: z.string(),
|
||||
state: z.string()
|
||||
})
|
||||
})
|
||||
|
||||
export type TUpdateCustomerInput = z.infer<typeof ZUpdateCustomerInput>;
|
||||
|
||||
export async function updateCustomer(data: TUpdateCustomerInput) {
|
||||
try {
|
||||
const premiumData = await PremiumModel.findOne({ user_id: data.user_id });
|
||||
if (!premiumData) throw new TRPCError({ code: 'NOT_FOUND', message: 'User premium data not found. Please contact support.' });
|
||||
if (!premiumData.customer_id) throw new TRPCError({ code: 'NOT_FOUND', message: 'Cannot find user customer. Please contact support.' });
|
||||
await StripeService.setCustomerInfo(premiumData.customer_id, data.address);
|
||||
return { ok: true }
|
||||
} catch (ex) {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: ex.message });
|
||||
}
|
||||
}
|
||||
|
||||
export const ZCreateCustomerInput = z.object({
|
||||
email: z.string()
|
||||
})
|
||||
|
||||
export type TCreateCustomerInput = z.infer<typeof ZCreateCustomerInput>;
|
||||
|
||||
export async function createCustomer(data: TCreateCustomerInput) {
|
||||
try {
|
||||
const customer = await StripeService.createCustomer(data.email);
|
||||
if (!customer) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Cannot create customer. Please contact support.' });
|
||||
return { ok: true, customer_id: customer.id }
|
||||
} catch (ex) {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: ex.message });
|
||||
}
|
||||
}
|
||||
|
||||
export const ZDeleteCustomerInput = z.object({
|
||||
customer_id: z.string()
|
||||
})
|
||||
|
||||
export type TDeleteCustomerInput = z.infer<typeof ZDeleteCustomerInput>;
|
||||
|
||||
export async function deleteCustomer(data: TDeleteCustomerInput) {
|
||||
try {
|
||||
const premiumData = await PremiumModel.findOne({ customer_id: data.customer_id });
|
||||
if (!premiumData) throw new TRPCError({ code: 'NOT_FOUND', message: 'User premium data not found. Please contact support.' });
|
||||
await StripeService.deleteCustomer(data.customer_id);
|
||||
return { ok: true }
|
||||
} catch (ex) {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: ex.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const ZListCustomerPaymentMethodsInput = z.object({
|
||||
customer_id: z.string()
|
||||
})
|
||||
|
||||
export type TListCustomerPaymentMethodsInput = z.infer<typeof ZListCustomerPaymentMethodsInput>;
|
||||
|
||||
export async function listCustomerPaymentMethods(data: TListCustomerPaymentMethodsInput) {
|
||||
try {
|
||||
const premiumData = await PremiumModel.findOne({ customer_id: data.customer_id });
|
||||
if (!premiumData) throw new TRPCError({ code: 'NOT_FOUND', message: 'User premium data not found. Please contact support.' });
|
||||
const methods = await StripeService.getCustomerPaymentMethods(data.customer_id);
|
||||
return methods;
|
||||
} catch (ex) {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: ex.message });
|
||||
}
|
||||
}
|
||||
49
payments/src/controllers/InvoiceController.ts
Normal file
49
payments/src/controllers/InvoiceController.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
|
||||
import { PremiumModel } from '../shared/schema/PremiumSchema';
|
||||
import StripeService from '../services/StripeService';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import z from 'zod';
|
||||
import { getPlanFromTag } from '../shared/data/PLANS';
|
||||
|
||||
export const ZGetInvoicesInput = z.object({
|
||||
user_id: z.string()
|
||||
})
|
||||
|
||||
export type TGetInvoicesInput = z.infer<typeof ZGetInvoicesInput>;
|
||||
|
||||
|
||||
export async function getInvoices(data: TGetInvoicesInput) {
|
||||
try {
|
||||
const premiumData = await PremiumModel.findOne({ user_id: data.user_id });
|
||||
if (!premiumData) throw new TRPCError({ code: 'NOT_FOUND', message: 'Cannot find user. Please contact support.' });
|
||||
if (!premiumData.customer_id) throw new TRPCError({ code: 'NOT_FOUND', message: 'Cannot find user customer. Please contact support.' });
|
||||
const invoices = await StripeService.getInvoices(premiumData.customer_id);
|
||||
return invoices;
|
||||
} catch (ex) {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: ex.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const ZCreateInvoiceInput = z.object({
|
||||
user_id: z.string(),
|
||||
customer_id: z.string(),
|
||||
})
|
||||
|
||||
export type TCreateInvoiceInput = z.infer<typeof ZCreateInvoiceInput>;
|
||||
|
||||
export async function createInvoice(data: TCreateInvoiceInput) {
|
||||
try {
|
||||
|
||||
const premiumData = await PremiumModel.findOne({ user_id: data.user_id, customer_id: data.customer_id });
|
||||
if (!premiumData) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot find premium data' });
|
||||
|
||||
const invoice = await StripeService.createInvoice(premiumData.customer_id);
|
||||
if (!invoice) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Cannot create preview' });
|
||||
|
||||
return invoice;
|
||||
|
||||
} catch (ex) {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: ex.message });
|
||||
}
|
||||
}
|
||||
170
payments/src/controllers/SubscriptionController.ts
Normal file
170
payments/src/controllers/SubscriptionController.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
|
||||
import StripeService from '../services/StripeService';
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import z from 'zod';
|
||||
import { addSubscriptionToUser } from './WebhookController';
|
||||
import { getPlanFromTag } from '../shared/data/PLANS';
|
||||
import { PremiumModel } from '../shared/schema/PremiumSchema';
|
||||
|
||||
|
||||
export const ZActivatePlanInput = z.object({
|
||||
user_id: z.string(),
|
||||
customer_id: z.string(),
|
||||
plan_tag: z.string()
|
||||
})
|
||||
|
||||
export type TActivatePlanInput = z.infer<typeof ZActivatePlanInput>;
|
||||
|
||||
export async function activatePlan(data: TActivatePlanInput) {
|
||||
try {
|
||||
|
||||
const exists = await PremiumModel.exists({ user_id: data.user_id });
|
||||
|
||||
if (!exists) {
|
||||
await PremiumModel.create({
|
||||
user_id: data.user_id,
|
||||
customer_id: data.customer_id,
|
||||
subscription_id: 'DUMMY',
|
||||
expire_at: Date.now() + 1000 * 60 * 60 * 24,
|
||||
});
|
||||
}
|
||||
|
||||
const subscription = await StripeService.createSubscription(data.customer_id, data.plan_tag);
|
||||
if (!subscription) {
|
||||
await PremiumModel.deleteOne({ user_id: data.user_id, customer_id: data.customer_id });
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Cannot create subscription. Please contact support.' });
|
||||
}
|
||||
|
||||
const plan = getPlanFromTag(data.plan_tag as any);
|
||||
if (!plan) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Cannot get plan. Please contact support.' });
|
||||
|
||||
await addSubscriptionToUser(data.user_id,
|
||||
plan,
|
||||
subscription.id,
|
||||
subscription.items.data[0].current_period_start,
|
||||
subscription.items.data[0].current_period_end
|
||||
);
|
||||
|
||||
return { ok: true }
|
||||
} catch (ex) {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: ex.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const ZCreateCheckoutInput = z.object({
|
||||
user_id: z.string(),
|
||||
customer_id: z.string(),
|
||||
plan_tag: z.string(),
|
||||
redirect_url: z.string()
|
||||
})
|
||||
|
||||
|
||||
export type TCreateCheckoutInput = z.infer<typeof ZCreateCheckoutInput>;
|
||||
|
||||
export async function createCheckout(data: TCreateCheckoutInput) {
|
||||
try {
|
||||
|
||||
const plan = getPlanFromTag(data.plan_tag as any);
|
||||
if (!plan) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot find plan' });
|
||||
|
||||
const premiumData = await PremiumModel.findOne({ user_id: data.user_id, customer_id: data.customer_id });
|
||||
if (!premiumData) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot find premium data' });
|
||||
|
||||
const price = StripeService.testMode ? plan.PRICE_TEST : plan.PRICE;
|
||||
|
||||
const checkout = await StripeService.createPayment(price, data.redirect_url, data.user_id, data.customer_id);
|
||||
if (!checkout) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Cannot create checkout' });
|
||||
|
||||
return checkout.url;
|
||||
|
||||
} catch (ex) {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: ex.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const ZCreatePreviewUpgradeInput = z.object({
|
||||
user_id: z.string(),
|
||||
customer_id: z.string(),
|
||||
plan_tag: z.string()
|
||||
})
|
||||
|
||||
|
||||
export type TCreatePreviewUpgradeInput = z.infer<typeof ZCreatePreviewUpgradeInput>;
|
||||
|
||||
export async function createPreviewUpgrade(data: TCreatePreviewUpgradeInput) {
|
||||
try {
|
||||
|
||||
const plan = getPlanFromTag(data.plan_tag as any);
|
||||
if (!plan) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot find plan' });
|
||||
|
||||
const premiumData = await PremiumModel.findOne({ user_id: data.user_id, customer_id: data.customer_id });
|
||||
if (!premiumData) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot find premium data' });
|
||||
|
||||
|
||||
const preview = await StripeService.getPreviewUpgrade(premiumData.customer_id, premiumData.subscription_id, plan.TAG);
|
||||
if (!preview) throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Cannot create preview' });
|
||||
|
||||
return preview;
|
||||
|
||||
} catch (ex) {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: ex.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const ZCreateUpgradeInput = z.object({
|
||||
user_id: z.string(),
|
||||
customer_id: z.string(),
|
||||
plan_tag: z.string()
|
||||
})
|
||||
|
||||
|
||||
export type TCreateUpgradeInput = z.infer<typeof ZCreateUpgradeInput>;
|
||||
|
||||
export async function createUpgrade(data: TCreateUpgradeInput) {
|
||||
try {
|
||||
|
||||
const plan = getPlanFromTag(data.plan_tag as any);
|
||||
if (!plan) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot find plan' });
|
||||
|
||||
const premiumData = await PremiumModel.findOne({ user_id: data.user_id, customer_id: data.customer_id });
|
||||
if (!premiumData) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot find premium data' });
|
||||
|
||||
|
||||
const result = await StripeService.updateSubscriptionWithPrice(data.customer_id, premiumData.subscription_id, data.plan_tag);
|
||||
return result;
|
||||
|
||||
} catch (ex) {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: ex.message });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export const ZCancelPlanInput = z.object({
|
||||
user_id: z.string(),
|
||||
customer_id: z.string()
|
||||
})
|
||||
|
||||
|
||||
export type TCancelPlanInput = z.infer<typeof ZCancelPlanInput>;
|
||||
|
||||
export async function cancelPlan(data: TCancelPlanInput) {
|
||||
try {
|
||||
|
||||
const premiumData = await PremiumModel.findOne({ user_id: data.user_id, customer_id: data.customer_id });
|
||||
if (!premiumData) throw new TRPCError({ code: 'BAD_REQUEST', message: 'Cannot find premium data' });
|
||||
|
||||
await StripeService.cancelPlan(data.customer_id);
|
||||
await PremiumModel.updateOne({ user_id: data.user_id, customer_id: data.customer_id }, { plan_cancelled: true });
|
||||
|
||||
return { ok: true }
|
||||
|
||||
} catch (ex) {
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: ex.message });
|
||||
}
|
||||
}
|
||||
179
payments/src/controllers/WebhookController.ts
Normal file
179
payments/src/controllers/WebhookController.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
|
||||
import type Event from 'stripe';
|
||||
import StripeService from '../services/StripeService';
|
||||
import { getPlanFromPrice, getPlanFromTag, PLAN_DATA } from '../shared/data/PLANS';
|
||||
import { PremiumModel } from '../shared/schema/PremiumSchema';
|
||||
import { UserLimitModel } from '../shared/schema/UserLimitSchema';
|
||||
import { email_client } from '../trcp_client';
|
||||
import { UserModel } from '../shared/schema/UserSchema';
|
||||
import { EmailNotifyModel } from '../shared/schema/emails/EmailNotifySchema';
|
||||
import mongoose from 'mongoose';
|
||||
import crypto from 'crypto';
|
||||
|
||||
|
||||
|
||||
export async function addSubscriptionToUser(user_id: string, plan: PLAN_DATA, subscription_id: string, current_period_start: number, current_period_end: number, payment_failed: boolean = false) {
|
||||
|
||||
console.log('Adding subscription to user', user_id, 'plan', plan.TAG, 'start', new Date(current_period_start * 1000).toLocaleString('it-IT'), 'end', new Date(current_period_end * 1000).toLocaleString('it-IT'));
|
||||
|
||||
await PremiumModel.updateOne({ user_id }, {
|
||||
premium_type: plan.ID,
|
||||
subscription_id,
|
||||
expire_at: current_period_end * 1000,
|
||||
plan_cancelled: false,
|
||||
payment_failed
|
||||
}, { upsert: true });
|
||||
|
||||
await UserLimitModel.updateOne({ user_id }, {
|
||||
events: 0,
|
||||
visits: 0,
|
||||
ai_messages: 0,
|
||||
limit: plan.COUNT_LIMIT,
|
||||
ai_limit: plan.AI_MESSAGE_LIMIT,
|
||||
billing_start_at: current_period_start * 1000,
|
||||
billing_expire_at: current_period_end * 1000,
|
||||
}, { upsert: true })
|
||||
|
||||
}
|
||||
|
||||
export async function onPaymentFailed(event: Event.InvoicePaymentFailedEvent) {
|
||||
|
||||
|
||||
if (event.data.object.attempt_count == 0) return { received: true, warn: 'attempt_count = 0' }
|
||||
|
||||
//TODO: Send emails
|
||||
|
||||
const customer_id = event.data.object.customer as string;
|
||||
|
||||
const premiumData = await PremiumModel.findOne({ customer_id });
|
||||
if (!premiumData) return { error: 'customer not found' }
|
||||
|
||||
await PremiumModel.updateOne({ customer_id }, { payment_failed: true });
|
||||
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
|
||||
export async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
|
||||
|
||||
|
||||
// Check selfhosted
|
||||
|
||||
const selfhostedProPrice = (event.data.object.lines.data.at(-1)?.pricing?.price_details?.price as string) === getPlanFromTag('SELFHOSTED_PRO')?.PRICE;
|
||||
|
||||
if (selfhostedProPrice) {
|
||||
|
||||
const code = crypto.randomBytes(4).toString('hex');
|
||||
|
||||
const email = event.data.object.customer_email;
|
||||
|
||||
if (!email) return { ok: false, data: 'selfhosted-pro', error: 'customer_email is undefined' }
|
||||
|
||||
await mongoose.connection.getClient().db('selfhosted-data').collection('pro-codes').insertOne({
|
||||
code,
|
||||
created_at: new Date(),
|
||||
email
|
||||
})
|
||||
|
||||
email_client.email.sendPurchaseSelfhostedEmail.mutate({ code, email });
|
||||
|
||||
return { ok: true, data: 'selfhosted-pro' };
|
||||
}
|
||||
|
||||
|
||||
const customer_id = event.data.object.customer as string;
|
||||
const premiumData = await PremiumModel.findOne({ customer_id });
|
||||
if (!premiumData) return { error: 'customer not found' }
|
||||
|
||||
if (event.data.object.status !== 'paid') return { received: true, warn: 'payment status not paid' }
|
||||
|
||||
const subscription_id = event.data.object.lines.data[0].subscription as string;
|
||||
|
||||
const price = event.data.object.lines.data.at(-1)?.pricing?.price_details?.price as string;
|
||||
if (!price) return { error: 'price not found' }
|
||||
|
||||
const plan = getPlanFromPrice(price, StripeService.testMode ?? true);
|
||||
if (!plan) return { error: 'plan not found' }
|
||||
|
||||
const databaseSubscription = premiumData.subscription_id;
|
||||
|
||||
const currentSubscriptionData = await StripeService.getSubscription(subscription_id);
|
||||
if (!currentSubscriptionData || currentSubscriptionData.status !== 'active') return { error: 'subscription not active' }
|
||||
|
||||
const FREE_TRIAL_PRICE = StripeService.testMode ? getPlanFromTag('FREE_TRIAL_LITLYX_PRO')?.PRICE_TEST : getPlanFromTag('FREE_TRIAL_LITLYX_PRO')?.PRICE;
|
||||
if (!FREE_TRIAL_PRICE) return { error: 'FREE_TRIAL_PRICE not found' }
|
||||
|
||||
|
||||
if (currentSubscriptionData.items.data[0].price.id === FREE_TRIAL_PRICE) {
|
||||
// Free trial
|
||||
|
||||
if (premiumData.expire_at < Date.now()) {
|
||||
// Free trial renew -> Free trial ended
|
||||
|
||||
try {
|
||||
await StripeService.deleteSubscription(databaseSubscription);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
const trialEndedSubscription = await StripeService.createSubscription(premiumData.customer_id, 'FREE_TRIAL_ENDED');
|
||||
|
||||
setTimeout(async () => {
|
||||
// FREE TRIAL ENDED MAIL
|
||||
const user = await UserModel.findOne({ _id: premiumData.user_id });
|
||||
if (!user) return { ok: false, error: 'CANNOT_FIND_USER' };
|
||||
const limitNotifies = await EmailNotifyModel.findOne({ user_id: premiumData.user_id });
|
||||
if (!limitNotifies) return { ok: false, error: 'CANNOT_FIND_LIMIT_NOTIFIES' };
|
||||
if (limitNotifies.n6) return { ok: false, error: 'ALREADY_SENDED' };
|
||||
await email_client.email.send_trial_6_ended.mutate({ email: user.email })
|
||||
await EmailNotifyModel.updateOne({ user_id: premiumData.user_id }, { n6: true });
|
||||
}, 1);
|
||||
|
||||
return { ok: true, action: 'FREE_TRIAL_ENDED', expired_at: new Date(premiumData.expire_at).toLocaleString('it-IT') };
|
||||
|
||||
} else {
|
||||
// Free trial activate
|
||||
return { ok: true, info: 'FREE_TRIAL_SUBSCRIPTION_ACTIVATED' };
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
// Other subscriptions
|
||||
|
||||
if (databaseSubscription != subscription_id) {
|
||||
try {
|
||||
await StripeService.deleteSubscription(databaseSubscription);
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
const start_period = event.data.object.billing_reason === 'subscription_create' ? event.data.object.lines.data[0].period.start :
|
||||
|
||||
(event.data.object.lines.data.length == 1 ? event.data.object.lines.data[0].period.start : event.data.object.lines.data[1].period.start)
|
||||
|
||||
const end_period = event.data.object.billing_reason === 'subscription_create' ? event.data.object.lines.data[0].period.end :
|
||||
(event.data.object.lines.data.length == 1 ? event.data.object.lines.data[0].period.end : event.data.object.lines.data[1].period.end)
|
||||
|
||||
await addSubscriptionToUser(premiumData.user_id.toString(), plan, subscription_id,
|
||||
start_period,
|
||||
end_period
|
||||
);
|
||||
|
||||
setTimeout(async () => {
|
||||
if (plan.ID == 7999) return;
|
||||
if (plan.ID == 0) return;
|
||||
if (plan.ID === premiumData.premium_type) return;
|
||||
// PURCHASE
|
||||
const user = await UserModel.findOne({ _id: premiumData.user_id });
|
||||
if (!user) {
|
||||
console.log({ ok: false, error: 'CANNOT_FIND_USER' });
|
||||
return { ok: false, error: 'CANNOT_FIND_USER' };
|
||||
}
|
||||
await email_client.email.sendPurchaseEmail.mutate({ email: user.email });
|
||||
}, 1);
|
||||
|
||||
return { ok: true, action: 'Add subscription ' + plan.TAG };
|
||||
}
|
||||
|
||||
}
|
||||
61
payments/src/index.ts
Normal file
61
payments/src/index.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import StripeService from './services/StripeService'
|
||||
import { router, createContext } from './trpc';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
|
||||
const app = express();
|
||||
app.use(cors());
|
||||
|
||||
import * as trpcExpress from '@trpc/server/adapters/express';
|
||||
import { connectDatabase } from './shared/services/DatabaseService';
|
||||
import { customerRouter } from './routers/CustomerRouter';
|
||||
import { invoiceRouter } from './routers/InvoiceRouter';
|
||||
import { subscriptionRouter } from './routers/SubscriptionRouter';
|
||||
import { webhookRouter } from './routers/WebhookRouter';
|
||||
import { startTickService } from './services/TickService';
|
||||
|
||||
if (!process.env.STRIPE_PRIVATE_KEY) throw Error('STRIPE_PRIVATE_KEY is required');
|
||||
if (!process.env.STRIPE_WEBHOOK_SECRET) throw Error('STRIPE_WEBHOOK_SECRET is required');
|
||||
if (!process.env.MONGO_CONNECTION_STRING) throw Error('MONGO_CONNECTION_STRING is required');
|
||||
if (!process.env.PORT) throw Error('PORT is required');
|
||||
if (!process.env.EMAIL_TRPC_URL) throw Error('EMAIL_TRPC_URL is required');
|
||||
if (!process.env.EMAIL_SECRET) throw Error('EMAIL_SECRET is required');
|
||||
|
||||
StripeService.init(process.env.STRIPE_PRIVATE_KEY, process.env.STRIPE_WEBHOOK_SECRET);
|
||||
connectDatabase(process.env.MONGO_CONNECTION_STRING);
|
||||
|
||||
console.log('Stripe started in', StripeService.testMode ? 'TESTMODE' : 'LIVEMODE');
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
const appRouter = router({
|
||||
customer: customerRouter,
|
||||
invoice: invoiceRouter,
|
||||
subscription: subscriptionRouter
|
||||
});
|
||||
|
||||
app.use((req, res, next) => {
|
||||
console.log(new Date().toLocaleString('it-IT'), req.method, req.path);
|
||||
next();
|
||||
});
|
||||
|
||||
app.use('/webhook', webhookRouter);
|
||||
|
||||
app.use('/trpc', trpcExpress.createExpressMiddleware({ router: appRouter, createContext }));
|
||||
|
||||
|
||||
const port = parseInt(process.env.PORT);
|
||||
if (!port) {
|
||||
console.error('PORT is not set');
|
||||
process.exit();
|
||||
}
|
||||
if (isNaN(port)) {
|
||||
console.error('PORT is not a valid number');
|
||||
process.exit();
|
||||
}
|
||||
|
||||
// BillingDeamonService.start();
|
||||
|
||||
startTickService();
|
||||
|
||||
app.listen(port, () => console.log(`[PAYMENTS] Listening on port ${port}`));
|
||||
28
payments/src/routers/CustomerRouter.ts
Normal file
28
payments/src/routers/CustomerRouter.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { protectedProcedure, router } from '../trpc';
|
||||
import { createCustomer, deleteCustomer, getCustomer, listCustomerPaymentMethods, updateCustomer, ZCreateCustomerInput, ZDeleteCustomerInput, ZGetCustomerInput, ZListCustomerPaymentMethodsInput, ZUpdateCustomerInput } from '../controllers/CustomerController';
|
||||
|
||||
|
||||
export const customerRouter = router({
|
||||
update: protectedProcedure.input(ZUpdateCustomerInput).mutation(async (opts) => {
|
||||
const result = await updateCustomer(opts.input);
|
||||
return result;
|
||||
}),
|
||||
get: protectedProcedure.input(ZGetCustomerInput).query(async (opts) => {
|
||||
const address = await getCustomer(opts.input);
|
||||
let result = { line1: '', line2: '', city: '', country: '', postal_code: '', state: '' };
|
||||
result = { ...result, ...address } as any
|
||||
return result;
|
||||
}),
|
||||
create: protectedProcedure.input(ZCreateCustomerInput).mutation(async (opts) => {
|
||||
const result = await createCustomer(opts.input);
|
||||
return result;
|
||||
}),
|
||||
delete: protectedProcedure.input(ZDeleteCustomerInput).mutation(async (opts) => {
|
||||
const result = await deleteCustomer(opts.input);
|
||||
return result;
|
||||
}),
|
||||
listMethods: protectedProcedure.input(ZListCustomerPaymentMethodsInput).query(async (opts) => {
|
||||
const result = await listCustomerPaymentMethods(opts.input);
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
10
payments/src/routers/InvoiceRouter.ts
Normal file
10
payments/src/routers/InvoiceRouter.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { protectedProcedure, router } from '../trpc';
|
||||
import { getInvoices, ZGetInvoicesInput } from '../controllers/InvoiceController';
|
||||
|
||||
|
||||
export const invoiceRouter = router({
|
||||
invoices: protectedProcedure.input(ZGetInvoicesInput).query(async (opts) => {
|
||||
const invoices = await getInvoices(opts.input);
|
||||
return invoices;
|
||||
}),
|
||||
});
|
||||
26
payments/src/routers/SubscriptionRouter.ts
Normal file
26
payments/src/routers/SubscriptionRouter.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { activatePlan, createCheckout, ZActivatePlanInput, ZCreateCheckoutInput, ZCreatePreviewUpgradeInput, createPreviewUpgrade, createUpgrade, ZCreateUpgradeInput, ZCancelPlanInput, cancelPlan } from '../controllers/SubscriptionController';
|
||||
import { protectedProcedure, router } from '../trpc';
|
||||
|
||||
|
||||
export const subscriptionRouter = router({
|
||||
activate: protectedProcedure.input(ZActivatePlanInput).mutation(async (opts) => {
|
||||
const result = await activatePlan(opts.input);
|
||||
return result;
|
||||
}),
|
||||
checkout: protectedProcedure.input(ZCreateCheckoutInput).mutation(async (opts) => {
|
||||
const result = await createCheckout(opts.input);
|
||||
return result;
|
||||
}),
|
||||
createPreviewUpgrade: protectedProcedure.input(ZCreatePreviewUpgradeInput).query(async (opts) => {
|
||||
const result = await createPreviewUpgrade(opts.input);
|
||||
return result;
|
||||
}),
|
||||
createUpgrade: protectedProcedure.input(ZCreateUpgradeInput).mutation(async (opts) => {
|
||||
const result = await createUpgrade(opts.input);
|
||||
return result;
|
||||
}),
|
||||
cancelPlan: protectedProcedure.input(ZCancelPlanInput).mutation(async (opts) => {
|
||||
const result = await cancelPlan(opts.input);
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
35
payments/src/routers/WebhookRouter.ts
Normal file
35
payments/src/routers/WebhookRouter.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
|
||||
import { raw, Router } from 'express';
|
||||
import { sendJson } from '../utils';
|
||||
import StripeService from '../services/StripeService';
|
||||
|
||||
import * as WebhookController from '../controllers/WebhookController'
|
||||
|
||||
export const webhookRouter = Router();
|
||||
|
||||
webhookRouter.post('/', raw({ type: 'application/json' }), async (req, res) => {
|
||||
try {
|
||||
|
||||
const signature = req.header('stripe-signature');
|
||||
if (!signature) return sendJson(res, 400, { error: 'No signature' });
|
||||
|
||||
const eventData = StripeService.parseWebhook(req.body, signature);
|
||||
if (!eventData) return sendJson(res, 400, { error: 'Error parsing event data' });
|
||||
|
||||
console.log('[WEBHOOK]', eventData.type);
|
||||
|
||||
if (eventData.type === 'invoice.paid') {
|
||||
const response = await WebhookController.onPaymentSuccess(eventData);
|
||||
return sendJson(res, 200, response);
|
||||
}
|
||||
|
||||
if (eventData.type === 'invoice.payment_failed') {
|
||||
const response = await WebhookController.onPaymentFailed(eventData);
|
||||
return sendJson(res, 200, response);
|
||||
}
|
||||
|
||||
|
||||
} catch (ex) {
|
||||
res.status(500).json({ error: ex.message });
|
||||
}
|
||||
});
|
||||
242
payments/src/services/StripeService.ts
Normal file
242
payments/src/services/StripeService.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
|
||||
import Stripe from "stripe";
|
||||
import { getPlanFromTag } from "../shared/data/PLANS";
|
||||
import { PremiumModel } from "../shared/schema/PremiumSchema";
|
||||
|
||||
class StripeService {
|
||||
private stripe?: Stripe;
|
||||
private privateKey?: string;
|
||||
private webhookSecret?: string;
|
||||
public testMode?: boolean;
|
||||
|
||||
init(privateKey: string, webhookSecret: string) {
|
||||
this.privateKey = privateKey;
|
||||
this.webhookSecret = webhookSecret;
|
||||
this.stripe = new Stripe(this.privateKey);
|
||||
this.testMode = this.privateKey.startsWith('sk_test');
|
||||
}
|
||||
|
||||
parseWebhook(body: any, sig: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
if (!this.webhookSecret) {
|
||||
console.error('Stripe not initialized')
|
||||
return;
|
||||
}
|
||||
return this.stripe.webhooks.constructEvent(body, sig, this.webhookSecret);
|
||||
}
|
||||
|
||||
|
||||
async createOnetimePayment(price: string, success_url: string, pid: string, customer?: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
|
||||
const checkout = await this.stripe.checkout.sessions.create({
|
||||
allow_promotion_codes: true,
|
||||
payment_method_types: ['card'],
|
||||
invoice_creation: {
|
||||
enabled: true,
|
||||
},
|
||||
line_items: [
|
||||
{ price, quantity: 1 }
|
||||
],
|
||||
payment_intent_data: {
|
||||
metadata: {
|
||||
pid, price
|
||||
}
|
||||
},
|
||||
customer,
|
||||
success_url,
|
||||
mode: 'payment'
|
||||
});
|
||||
|
||||
return checkout;
|
||||
}
|
||||
|
||||
async createPayment(price: string, success_url: string, user_id: string, customer: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
|
||||
const checkout = await this.stripe.checkout.sessions.create({
|
||||
allow_promotion_codes: true,
|
||||
payment_method_types: ['card'],
|
||||
line_items: [
|
||||
{ price, quantity: 1 }
|
||||
],
|
||||
subscription_data: {
|
||||
metadata: { user_id },
|
||||
},
|
||||
customer,
|
||||
success_url,
|
||||
mode: 'subscription'
|
||||
});
|
||||
|
||||
return checkout;
|
||||
}
|
||||
|
||||
async getPriceData(priceId: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
const priceData = await this.stripe.prices.retrieve(priceId);
|
||||
return priceData;
|
||||
}
|
||||
|
||||
async deleteSubscription(subscriptionId: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
const subscription = await this.stripe.subscriptions.cancel(subscriptionId);
|
||||
return subscription;
|
||||
}
|
||||
|
||||
async getSubscription(subscriptionId: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId);
|
||||
return subscription;
|
||||
}
|
||||
|
||||
async getAllSubscriptions(customer_id: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
const subscriptions = await this.stripe.subscriptions.list({ customer: customer_id });
|
||||
return subscriptions;
|
||||
}
|
||||
|
||||
async getInvoices(customer_id: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
const invoices = await this.stripe?.invoices.list({ customer: customer_id });
|
||||
return invoices;
|
||||
}
|
||||
|
||||
async getCustomer(customer_id: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
const customer = await this.stripe.customers.retrieve(customer_id, { expand: [] })
|
||||
return customer;
|
||||
}
|
||||
|
||||
async getCustomerPaymentMethods(customer_id: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
const paymentMethods = await this.stripe.customers.listPaymentMethods(customer_id);
|
||||
return paymentMethods;
|
||||
}
|
||||
|
||||
async createCustomer(email: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
const customer = await this.stripe.customers.create({ email });
|
||||
return customer;
|
||||
}
|
||||
|
||||
async setCustomerInfo(customer_id: string, address: { line1: string, line2: string, city: string, country: string, postal_code: string, state: string }) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
const customer = await this.stripe.customers.update(customer_id, {
|
||||
address: {
|
||||
line1: address.line1,
|
||||
line2: address.line2,
|
||||
city: address.city,
|
||||
country: address.country,
|
||||
postal_code: address.postal_code,
|
||||
state: address.state
|
||||
}
|
||||
})
|
||||
return customer.id;
|
||||
}
|
||||
|
||||
async deleteCustomer(customer_id: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
const { deleted } = await this.stripe.customers.del(customer_id);
|
||||
return deleted;
|
||||
}
|
||||
|
||||
async createSubscription(customer_id: string, planTag: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
|
||||
const PLAN_DATA = getPlanFromTag(planTag as any);
|
||||
if (!PLAN_DATA) throw Error('Plan not found');
|
||||
|
||||
const subscription = await this.stripe.subscriptions.create({
|
||||
customer: customer_id,
|
||||
items: [
|
||||
{ price: this.testMode ? PLAN_DATA.PRICE_TEST : PLAN_DATA.PRICE, quantity: 1 }
|
||||
],
|
||||
});
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
async getPreviewUpgrade(customer_id: string, subscription_id: string, planTag: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
|
||||
const PLAN_DATA = getPlanFromTag(planTag as any);
|
||||
if (!PLAN_DATA) throw Error('Plan not found');
|
||||
|
||||
const premiumData = await PremiumModel.findOne({ customer_id });
|
||||
if (!premiumData) throw Error('Plan not found');
|
||||
|
||||
const currentSubscription = await this.stripe.subscriptions.retrieve(premiumData?.subscription_id)
|
||||
if (!premiumData) throw Error('Subscription not found');
|
||||
|
||||
const preview = await this.stripe.invoices.createPreview({
|
||||
customer: customer_id,
|
||||
subscription: subscription_id,
|
||||
subscription_details: {
|
||||
items: [
|
||||
{ id: currentSubscription.items.data[0].id, price: this.testMode ? PLAN_DATA.PRICE_TEST : PLAN_DATA.PRICE, quantity: 1 }
|
||||
],
|
||||
proration_date: Math.floor(Date.now() / 1000),
|
||||
proration_behavior: 'always_invoice'
|
||||
}
|
||||
});
|
||||
|
||||
return preview;
|
||||
}
|
||||
|
||||
async createInvoice(customer_id: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
|
||||
const premiumData = await PremiumModel.findOne({ customer_id });
|
||||
if (!premiumData) throw Error('Plan not found');
|
||||
|
||||
const currentSubscription = await this.stripe.subscriptions.retrieve(premiumData?.subscription_id)
|
||||
if (!premiumData) throw Error('Subscription not found');
|
||||
|
||||
const invoice = await this.stripe.invoices.create({
|
||||
customer: customer_id,
|
||||
subscription: currentSubscription.id,
|
||||
});
|
||||
|
||||
if (!invoice || !invoice.id) throw Error('Cannot create invoice');
|
||||
|
||||
const finalized = this.stripe.invoices.finalizeInvoice(invoice.id);
|
||||
|
||||
return finalized;
|
||||
}
|
||||
|
||||
async updateSubscriptionWithPrice(customer_id: string, subscription_id: string, plan_tag: string) {
|
||||
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
|
||||
const PLAN_DATA = getPlanFromTag(plan_tag as any);
|
||||
if (!PLAN_DATA) throw Error('Plan not found');
|
||||
|
||||
const premiumData = await PremiumModel.findOne({ customer_id });
|
||||
if (!premiumData) throw Error('Plan not found');
|
||||
|
||||
const currentSubscription = await this.stripe.subscriptions.retrieve(premiumData?.subscription_id)
|
||||
if (!premiumData) throw Error('Subscription not found');
|
||||
|
||||
this.stripe.subscriptions.update(subscription_id, {
|
||||
items: [
|
||||
{ id: currentSubscription.items.data[0].id, price: this.testMode ? PLAN_DATA.PRICE_TEST : PLAN_DATA.PRICE, quantity: 1 }
|
||||
],
|
||||
proration_date: Math.floor(Date.now() / 1000),
|
||||
proration_behavior: 'always_invoice'
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
async cancelPlan(customer_id: string) {
|
||||
if (!this.stripe) throw Error('Stripe not initialized');
|
||||
const paymentMethods = await this.stripe.customers.listPaymentMethods(customer_id);
|
||||
for (const paymentMethod of paymentMethods.data) {
|
||||
await this.stripe.paymentMethods.detach(paymentMethod.id);
|
||||
}
|
||||
return { ok: true, count: paymentMethods.data.length }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const instance = new StripeService();
|
||||
export default instance;
|
||||
163
payments/src/services/TickService.ts
Normal file
163
payments/src/services/TickService.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { CronJob } from 'cron';
|
||||
import { PremiumModel } from '../shared/schema/PremiumSchema';
|
||||
import { EmailNotifyModel, TEmailNotify } from '../shared/schema/emails/EmailNotifySchema';
|
||||
import { email_client } from '../trcp_client';
|
||||
import { UserModel } from '../shared/schema/UserSchema';
|
||||
import { UserLimitModel } from '../shared/schema/UserLimitSchema';
|
||||
import { PLAN_DATA, PLAN_DATA_MAP, PLAN_TAG, PLAN_TAGS, PREMIUM_PLAN } from '../shared/data/PLANS';
|
||||
|
||||
|
||||
export async function startTickService() {
|
||||
const job = new CronJob('0 */6 * * *', () => {
|
||||
console.log(new Date().toLocaleString('it-IT'), 'JOB STARTED');
|
||||
manageFreeTrials(0, 20);
|
||||
manageFreeTrialsEnded(0, 20);
|
||||
});
|
||||
job.start();
|
||||
}
|
||||
|
||||
const DAY = 1000 * 60 * 60 * 24;
|
||||
const MONTH = DAY * 30;
|
||||
|
||||
|
||||
async function getBestPlanAndVisits(user_id: string) {
|
||||
const limits = await UserLimitModel.findOne({ user_id });
|
||||
if (!limits) return { visits: 0, plan: 'Mini' };
|
||||
const { visits } = limits;
|
||||
|
||||
const planKey = Object.keys(PREMIUM_PLAN).find(e => {
|
||||
const target: PLAN_DATA = PREMIUM_PLAN[e];
|
||||
if (target.ID <= 8000) return false;
|
||||
if (target.COUNT_LIMIT > visits) return true;
|
||||
});
|
||||
|
||||
if (!planKey) return { visits: 0, plan: 'Mini' };
|
||||
|
||||
return { visits, plan: PREMIUM_PLAN[planKey].NAME }
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
async function manageFreeTrials(skip: number, limit: number) {
|
||||
const premiumData = await PremiumModel.find({ premium_type: 7006 }, {}, { skip, limit });
|
||||
|
||||
console.log('Finding free trials', { skip, limit }, { count: premiumData.length });
|
||||
|
||||
if (premiumData.length == 0) return;
|
||||
for (const premiumItem of premiumData) {
|
||||
|
||||
const user = await UserModel.findOne({ _id: premiumItem.user_id });
|
||||
if (!user) continue;
|
||||
|
||||
let notifyData = await EmailNotifyModel.findOne({ user_id: premiumItem.user_id });
|
||||
|
||||
if (!notifyData) {
|
||||
await EmailNotifyModel.create({
|
||||
user_id: premiumItem.user_id,
|
||||
n1: false, n2: false, n3: false, n4: false,
|
||||
n5: false, n6: false, n7: false, n8: false
|
||||
});
|
||||
notifyData = await EmailNotifyModel.findOne({ user_id: premiumItem.user_id });
|
||||
}
|
||||
if (!notifyData) continue;
|
||||
|
||||
|
||||
const started_at_timestamp = new Date(premiumItem.expire_at).getTime() - MONTH;
|
||||
const end_at_timestamp = new Date(premiumItem.expire_at).getTime();
|
||||
|
||||
// console.log(premiumItem._id.toString(), new Date(started_at_timestamp).toLocaleString('it-IT'), new Date(end_at_timestamp).toLocaleString('it-IT'));
|
||||
|
||||
|
||||
if (Date.now() >= end_at_timestamp - DAY) {
|
||||
const data = await getBestPlanAndVisits(premiumItem.user_id.toString());
|
||||
await checkEmail_N5_today(notifyData, premiumItem.user_id.toString(), user.email, data.plan, data.visits);
|
||||
} else if (Date.now() >= end_at_timestamp - DAY * 2) {
|
||||
const data = await getBestPlanAndVisits(premiumItem.user_id.toString());
|
||||
await checkEmail_N4_tomorrow(notifyData, premiumItem.user_id.toString(), user.email, data.plan, data.visits);
|
||||
} else if (Date.now() >= end_at_timestamp - DAY * 7) {
|
||||
await checkEmail_N3_1WeekLeft(notifyData, premiumItem.user_id.toString(), user.email);
|
||||
} else if (Date.now() - started_at_timestamp >= DAY * 10) {
|
||||
await checkEmail_N2_10DaysIn(notifyData, premiumItem.user_id.toString(), user.email);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
manageFreeTrials(skip + limit, limit);
|
||||
}, 1)
|
||||
}
|
||||
|
||||
async function manageFreeTrialsEnded(skip: number, limit: number) {
|
||||
const premiumData = await PremiumModel.find({ premium_type: 7999 }, {}, { skip, limit });
|
||||
|
||||
console.log('Finding free trials ended', { skip, limit }, { count: premiumData.length });
|
||||
|
||||
if (premiumData.length == 0) return;
|
||||
for (const premiumItem of premiumData) {
|
||||
|
||||
const user = await UserModel.findOne({ _id: premiumItem.user_id });
|
||||
if (!user) continue;
|
||||
const notifyData = await EmailNotifyModel.findOne({ user_id: premiumItem.user_id });
|
||||
if (!notifyData) continue;
|
||||
|
||||
const started_at_timestamp = new Date(premiumItem.expire_at).getTime() - MONTH * 12;
|
||||
const end_at_timestamp = new Date(premiumItem.expire_at).getTime();
|
||||
|
||||
// console.log(premiumItem._id.toString(), new Date(started_at_timestamp).toLocaleString('it-IT'), new Date(end_at_timestamp).toLocaleString('it-IT'));
|
||||
|
||||
if (Date.now() >= started_at_timestamp + DAY * 12) {
|
||||
await checkEmail_N7_will_stop(notifyData, premiumItem.user_id.toString(), user.email);
|
||||
}
|
||||
if (Date.now() >= started_at_timestamp + DAY * 14) {
|
||||
await checkEmail_N8_stop_grace(notifyData, premiumItem.user_id.toString(), user.email);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
manageFreeTrialsEnded(skip + limit, limit);
|
||||
}, 1)
|
||||
|
||||
}
|
||||
|
||||
async function checkEmail_N2_10DaysIn(notifyData: TEmailNotify, user_id: string, email: string) {
|
||||
if (notifyData.n2) return;
|
||||
console.log('SENDING: send_trial_2_10_days_in', email);
|
||||
await email_client.email.send_trial_2_10_days_in.mutate({ email });
|
||||
await EmailNotifyModel.updateOne({ user_id }, { n2: true });
|
||||
}
|
||||
|
||||
async function checkEmail_N3_1WeekLeft(notifyData: TEmailNotify, user_id: string, email: string) {
|
||||
if (notifyData.n3) return;
|
||||
console.log('SENDING: send_trial_3_1_week_left', email);
|
||||
await email_client.email.send_trial_3_1_week_left.mutate({ email });
|
||||
await EmailNotifyModel.updateOne({ user_id }, { n3: true });
|
||||
}
|
||||
|
||||
async function checkEmail_N4_tomorrow(notifyData: TEmailNotify, user_id: string, email: string, plan: string, visits: number) {
|
||||
if (notifyData.n4) return;
|
||||
console.log('SENDING: send_trial_4_ends_tomorrow', email);
|
||||
await email_client.email.send_trial_4_ends_tomorrow.mutate({ email, plan, visits });
|
||||
await EmailNotifyModel.updateOne({ user_id }, { n4: true });
|
||||
}
|
||||
|
||||
async function checkEmail_N5_today(notifyData: TEmailNotify, user_id: string, email: string, plan: string, visits: number) {
|
||||
if (notifyData.n5) return;
|
||||
console.log('SENDING: send_trial_5_ends_today', email);
|
||||
await email_client.email.send_trial_5_ends_today.mutate({ email, plan, visits });
|
||||
await EmailNotifyModel.updateOne({ user_id }, { n5: true });
|
||||
}
|
||||
|
||||
async function checkEmail_N7_will_stop(notifyData: TEmailNotify, user_id: string, email: string) {
|
||||
if (notifyData.n7) return;
|
||||
console.log('SENDING: send_trial_7_stop_collecting', email);
|
||||
await email_client.email.send_trial_7_stop_collecting.mutate({ email });
|
||||
await EmailNotifyModel.updateOne({ user_id }, { n7: true });
|
||||
}
|
||||
|
||||
async function checkEmail_N8_stop_grace(notifyData: TEmailNotify, user_id: string, email: string) {
|
||||
if (notifyData.n8) return;
|
||||
console.log('SENDING: send_trial_8_stop_grace_period', email);
|
||||
await email_client.email.send_trial_8_stop_grace_period.mutate({ email });
|
||||
await EmailNotifyModel.updateOne({ user_id }, { n8: true });
|
||||
}
|
||||
15
payments/src/trcp_client.ts
Normal file
15
payments/src/trcp_client.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
||||
import type { AppRouter } from '../../emails/src/index';
|
||||
|
||||
export const email_client = createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: process.env.EMAIL_TRPC_URL as string,
|
||||
async headers() {
|
||||
return {
|
||||
Authorization: `Bearer ${process.env.EMAIL_SECRET as string}`
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
28
payments/src/trpc.ts
Normal file
28
payments/src/trpc.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { initTRPC, TRPCError } from '@trpc/server';
|
||||
import { CreateHTTPContextOptions } from '@trpc/server/adapters/standalone';
|
||||
|
||||
const t = initTRPC.context<Context>().create()
|
||||
|
||||
export const router = t.router;
|
||||
const publicProcedure = t.procedure;
|
||||
|
||||
export type Context = ReturnType<typeof createContext>;
|
||||
|
||||
export function createContext({ req }: CreateHTTPContextOptions) {
|
||||
return { headers: req.headers }
|
||||
}
|
||||
|
||||
export const protectedProcedure = publicProcedure.use(async ({ ctx, next }) => {
|
||||
try {
|
||||
const headers = ctx.headers;
|
||||
const authorization = headers.authorization;
|
||||
if (!authorization) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Missing Authorization header' });
|
||||
const [mode, content] = authorization.split(' ');
|
||||
if (mode !== 'Bearer') throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Authorization type not valid' });
|
||||
if (content !== process.env.PAYMENT_SECRET) throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Authorization token invalid', });
|
||||
return next();
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Error during authorization' });
|
||||
}
|
||||
});
|
||||
6
payments/src/utils.ts
Normal file
6
payments/src/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { Response } from "express";
|
||||
|
||||
|
||||
export function sendJson(res: Response, status: number, data: Record<string, any>): void {
|
||||
res.status(status).json(data);
|
||||
}
|
||||
19
payments/tsconfig.json
Normal file
19
payments/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "NodeNext",
|
||||
"target": "ESNext",
|
||||
"outDir": "dist",
|
||||
"strictNullChecks": true,
|
||||
"moduleResolution": "nodenext",
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"importsNotUsedAsValues": "remove",
|
||||
"preserveValueImports": false
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user