new selfhosted version

This commit is contained in:
antonio
2025-11-28 14:11:51 +01:00
parent afda29997d
commit 951860f67e
1046 changed files with 72586 additions and 574750 deletions

3
payments/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules
src/shared
dist

31
payments/package.json Normal file
View 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
View 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: {}

View 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 });
}
}

View 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 });
}
}

View 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 });
}
}

View 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
View 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}`));

View 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;
}),
});

View 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;
}),
});

View 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;
}),
});

View 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 });
}
});

View 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;

View 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 });
}

View 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
View 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
View 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
View 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"
]
}