From 1f9ef5d18c796e16075754895384d725ba51e031 Mon Sep 17 00:00:00 2001 From: Emily Date: Wed, 26 Mar 2025 15:30:22 +0100 Subject: [PATCH] add payment service --- package.json | 3 + payments/.gitignore | 9 + payments/package.json | 28 + payments/pnpm-lock.yaml | 901 +++++++++++++++++++++++++ payments/src/Utils.ts | 6 + payments/src/index.ts | 0 payments/src/routers/PaymentRouter.ts | 43 ++ payments/src/routers/WebhookRouter.ts | 19 + payments/src/services/StripeService.ts | 209 ++++++ payments/tsconfig.json | 13 + scripts/payments/deploy.ts | 87 +++ scripts/payments/shared.ts | 23 + shared_global/data/PLANS.ts | 201 ++++++ 13 files changed, 1542 insertions(+) create mode 100644 payments/.gitignore create mode 100644 payments/package.json create mode 100644 payments/pnpm-lock.yaml create mode 100644 payments/src/Utils.ts create mode 100644 payments/src/index.ts create mode 100644 payments/src/routers/PaymentRouter.ts create mode 100644 payments/src/routers/WebhookRouter.ts create mode 100644 payments/src/services/StripeService.ts create mode 100644 payments/tsconfig.json create mode 100644 scripts/payments/deploy.ts create mode 100644 scripts/payments/shared.ts create mode 100644 shared_global/data/PLANS.ts diff --git a/package.json b/package.json index f195002..4a95292 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,9 @@ "consumer:shared": "ts-node scripts/consumer/shared.ts", "consumer:deploy": "ts-node scripts/consumer/deploy.ts", + "payments:shared": "ts-node scripts/payments/shared.ts", + "payments:deploy": "ts-node scripts/payments/deploy.ts", + "email:deploy": "ts-node scripts/email/deploy.ts" }, "keywords": [], diff --git a/payments/.gitignore b/payments/.gitignore new file mode 100644 index 0000000..15f4418 --- /dev/null +++ b/payments/.gitignore @@ -0,0 +1,9 @@ +node_modules +static +ecosystem.config.cjs +ecosystem.config.js +dist +start_dev.js +package-lock.json +build_all.bat +src/shared \ No newline at end of file diff --git a/payments/package.json b/payments/package.json new file mode 100644 index 0000000..2b440cb --- /dev/null +++ b/payments/package.json @@ -0,0 +1,28 @@ +{ + "name": "payments", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "dev": "node scripts/start_dev.js", + "dev_prod": "node scripts/start_dev_prod.js", + "compile": "tsc", + "build": "npm run compile", + "workspace:shared": "ts-node ../scripts/payments/shared.ts", + "workspace:deploy": "ts-node ../scripts/payments/deploy.ts" + }, + "keywords": [], + "author": "Emily", + "license": "MIT", + "dependencies": { + "@types/express": "^5.0.1", + "cors": "^2.8.5", + "express": "^4.21.2", + "mongoose": "^8.13.0", + "stripe": "^17.7.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "@types/cors": "^2.8.17" + } +} \ No newline at end of file diff --git a/payments/pnpm-lock.yaml b/payments/pnpm-lock.yaml new file mode 100644 index 0000000..e476994 --- /dev/null +++ b/payments/pnpm-lock.yaml @@ -0,0 +1,901 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@types/express': + specifier: ^5.0.1 + version: 5.0.1 + cors: + specifier: ^2.8.5 + version: 2.8.5 + express: + specifier: ^4.21.2 + version: 4.21.2 + mongoose: + specifier: ^8.13.0 + version: 8.13.0 + stripe: + specifier: ^17.7.0 + version: 17.7.0 + zod: + specifier: ^3.24.2 + version: 3.24.2 + devDependencies: + '@types/cors': + specifier: ^2.8.17 + version: 2.8.17 + +packages: + + '@mongodb-js/saslprep@1.2.0': + resolution: {integrity: sha512-+ywrb0AqkfaYuhHs6LxKWgqbh3I72EpEgESCw37o+9qPx9WTCkgDm2B+eMrwehGtHBWHFU4GXvnSCNiFhhausg==} + + '@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.17': + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + + '@types/express-serve-static-core@5.0.6': + resolution: {integrity: sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==} + + '@types/express@5.0.1': + resolution: {integrity: sha512-UZUw8vjpWFXuDnjFTh7/5c2TWDlQqeXHi6hcN7F2XSVT5P+WmUnnbFS3KA6Jnc6IsEqI2qCVu2bK0R0J4A8ZQQ==} + + '@types/http-errors@2.0.4': + resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/node@22.13.13': + resolution: {integrity: sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==} + + '@types/qs@6.9.18': + resolution: {integrity: sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==} + + '@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@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + 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@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} + 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'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + 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@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + 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@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + 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.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + 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'} + + kareem@2.6.3: + resolution: {integrity: sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==} + engines: {node: '>=12.0.0'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + memory-pager@1.5.0: + resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mongodb-connection-string-url@3.0.2: + resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==} + + mongodb@6.15.0: + resolution: {integrity: sha512-ifBhQ0rRzHDzqp9jAQP6OwHSH7dbYIQjD3SbJs9YYk9AikKEettW/9s/tbSFDTpXcRbF+u1aLrhHxDFaYtZpFQ==} + 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.13.0: + resolution: {integrity: sha512-e/iYV1mPeOkg+SWAMHzt3t42/EZyER3OB1H2pjP9C3vQ+Qb5DMeV9Kb+YCUycKgScA3fbwL7dKG4EpinGlg21g==} + 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.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + 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'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + 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.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.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@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + 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@17.7.0: + resolution: {integrity: sha512-aT2BU9KkizY9SATf14WhhYVv2uOapBWX0OFWF4xvcj1mPaNotlSc2CsxpS4DS46ZueSppmCF5BX1sNYBtwBvfw==} + engines: {node: '>=12.*'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tr46@5.1.0: + resolution: {integrity: sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==} + engines: {node: '>=18'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + 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'} + + zod@3.24.2: + resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + +snapshots: + + '@mongodb-js/saslprep@1.2.0': + dependencies: + sparse-bitfield: 3.0.3 + + '@types/body-parser@1.19.5': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.13.13 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.13.13 + + '@types/cors@2.8.17': + dependencies: + '@types/node': 22.13.13 + + '@types/express-serve-static-core@5.0.6': + dependencies: + '@types/node': 22.13.13 + '@types/qs': 6.9.18 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.4 + + '@types/express@5.0.1': + 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/mime@1.3.5': {} + + '@types/node@22.13.13': + dependencies: + undici-types: 6.20.0 + + '@types/qs@6.9.18': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.4': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.13.13 + + '@types/serve-static@1.15.7': + dependencies: + '@types/http-errors': 2.0.4 + '@types/node': 22.13.13 + '@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@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + array-flatten@1.1.1: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + 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@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + destroy@1.2.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@1.0.2: {} + + 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@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + 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.4.24: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + kareem@2.6.3: {} + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + memory-pager@1.5.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mongodb-connection-string-url@3.0.2: + dependencies: + '@types/whatwg-url': 11.0.5 + whatwg-url: 14.2.0 + + mongodb@6.15.0: + dependencies: + '@mongodb-js/saslprep': 1.2.0 + bson: 6.10.3 + mongodb-connection-string-url: 3.0.2 + + mongoose@8.13.0: + dependencies: + bson: 6.10.3 + kareem: 2.6.3 + mongodb: 6.15.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.0 + transitivePeerDependencies: + - supports-color + + ms@2.0.0: {} + + ms@2.1.3: {} + + negotiator@0.6.3: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + parseurl@1.3.3: {} + + path-to-regexp@0.1.12: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + punycode@2.3.1: {} + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.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@17.7.0: + dependencies: + '@types/node': 22.13.13 + qs: 6.14.0 + + toidentifier@1.0.1: {} + + tr46@5.1.0: + dependencies: + punycode: 2.3.1 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + undici-types@6.20.0: {} + + unpipe@1.0.0: {} + + utils-merge@1.0.1: {} + + vary@1.1.2: {} + + webidl-conversions@7.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.0 + webidl-conversions: 7.0.0 + + zod@3.24.2: {} diff --git a/payments/src/Utils.ts b/payments/src/Utils.ts new file mode 100644 index 0000000..2c60266 --- /dev/null +++ b/payments/src/Utils.ts @@ -0,0 +1,6 @@ +import type { Response } from "express"; + + +export function sendJson(res: Response, status: number, data: Record): void { + res.status(status).json(data); +} \ No newline at end of file diff --git a/payments/src/index.ts b/payments/src/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/payments/src/routers/PaymentRouter.ts b/payments/src/routers/PaymentRouter.ts new file mode 100644 index 0000000..737c28e --- /dev/null +++ b/payments/src/routers/PaymentRouter.ts @@ -0,0 +1,43 @@ +import { json, Router } from 'express'; +import z from 'zod'; +import { getPlanFromId } from '../shared/data/PLANS'; +import StripeService from '../services/StripeService'; +import { sendJson } from '../Utils'; +import { ProjectModel } from '../shared/schema/project/ProjectSchema'; + +export const paymentRouter = Router(); + + +export const ZBodyCreatePayment = z.object({ + pid: z.string(), + plan_id: z.number() +}) + +paymentRouter.post('/create', json(), async (req, res) => { + try { + const createPaymentData = ZBodyCreatePayment.parse(req.body); + + const plan = getPlanFromId(createPaymentData.plan_id); + if (!plan) return sendJson(res, 400, { error: 'plan not found' }); + + const project = await ProjectModel.findById(createPaymentData.pid); + if (!project) return sendJson(res, 400, { error: 'project not found' }); + if (!project.customer_id) return sendJson(res, 400, { error: 'project have no customer_id' }); + + const price = StripeService.testMode ? plan.PRICE_TEST : plan.PRICE; + + const checkout = await StripeService.createPayment( + price, + 'https://dashboard.litlyx.com/payment_ok', + createPaymentData.pid, + project.customer_id + ); + + if (!checkout) return sendJson(res, 400, { error: 'cannot create payment' }); + + return sendJson(res, 200, { url: checkout.url }); + + } catch (ex) { + res.status(500).json({ error: ex.message }); + } +}); \ No newline at end of file diff --git a/payments/src/routers/WebhookRouter.ts b/payments/src/routers/WebhookRouter.ts new file mode 100644 index 0000000..cad316f --- /dev/null +++ b/payments/src/routers/WebhookRouter.ts @@ -0,0 +1,19 @@ + +import { json, Router } from 'express'; + +export const webhookRouter = Router(); + + +webhookRouter.get('/', json(), async (req, res) => { + try { + + const signature = req.header('stripe-signature'); + if (!signature) { + console.error('No signature on the webhook') + } + + + } catch (ex) { + res.status(500).json({ error: ex.message }); + } +}); \ No newline at end of file diff --git a/payments/src/services/StripeService.ts b/payments/src/services/StripeService.ts new file mode 100644 index 0000000..5cd22d8 --- /dev/null +++ b/payments/src/services/StripeService.ts @@ -0,0 +1,209 @@ + +import Stripe from "stripe"; + + + +class StripeService { + private stripe?: Stripe; + private privateKey?: string; + private webhookSecret?: string; + public testMode?: boolean; + + init(privateKey: string, webhookSecret: string, testMode: boolean = false) { + this.privateKey = privateKey; + this.webhookSecret = webhookSecret; + this.stripe = new Stripe(this.privateKey); + this.testMode = testMode; + } + + 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, 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'], + line_items: [ + { price, quantity: 1 } + ], + subscription_data: { + metadata: { pid }, + }, + 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 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 createStripeCode(plan: PREMIUM_TAG) { + // if (!this.stripe) throw Error('Stripe not initialized'); + + // const INCUBATION_COUPON = 'sDD7Weh3'; + + // if (plan === 'INCUBATION') { + // await this.stripe.promotionCodes.create({ + // coupon: INCUBATION_COUPON, + // active: true, + // code: 'TESTCACCA1', + // max_redemptions: 1, + // }) + // return true; + // } + + // return false; + // } + + // async createSubscription(customer_id: string, planId: number) { + // if (this.disabledMode) return; + // if (!this.stripe) throw Error('Stripe not initialized'); + + // const PLAN = getPlanFromId(planId); + // if (!PLAN) throw Error('Plan not found'); + + // const subscription = await this.stripe.subscriptions.create({ + // customer: customer_id, + // items: [ + // { price: this.testMode ? PLAN.PRICE_TEST : PLAN.PRICE, quantity: 1 } + // ], + // }); + + // return subscription; + // } + + // async createOneTimeSubscriptionDummy(customer_id: string, planId: number) { + // if (this.disabledMode) return; + // if (!this.stripe) throw Error('Stripe not initialized'); + + // const PLAN = getPlanFromId(planId); + // if (!PLAN) throw Error('Plan not found'); + + // const subscription = await this.stripe.subscriptions.create({ + // customer: customer_id, + // items: [ + // { price: this.testMode ? PLAN.PRICE_TEST : PLAN.PRICE, quantity: 1 } + // ], + // }); + + // return subscription; + // } + + // async createFreeSubscription(customer_id: string) { + // if (this.disabledMode) return; + // if (!this.stripe) throw Error('Stripe not initialized'); + + // const FREE_PLAN = getPlanFromTag('FREE'); + + // const subscription = await this.stripe.subscriptions.create({ + // customer: customer_id, + // items: [ + // { price: this.testMode ? FREE_PLAN.PRICE_TEST : FREE_PLAN.PRICE, quantity: 1 } + // ] + // }); + + // return subscription; + + // } + +} + +const instance = new StripeService(); +export default instance; diff --git a/payments/tsconfig.json b/payments/tsconfig.json new file mode 100644 index 0000000..b8d10c4 --- /dev/null +++ b/payments/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "target": "ESNext", + "outDir": "dist" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/scripts/payments/deploy.ts b/scripts/payments/deploy.ts new file mode 100644 index 0000000..012307c --- /dev/null +++ b/scripts/payments/deploy.ts @@ -0,0 +1,87 @@ + +// import fs from 'fs-extra'; +// import path from 'path'; +// import child from 'child_process'; +// import { createZip } from '../helpers/zip-helper'; +// import { DeployHelper } from '../helpers/deploy-helper'; +// import { DATABASE_CONNECTION_STRING_PRODUCTION, DATABASE_CONNECTION_STRING_TESTMODE, REMOTE_HOST_TESTMODE } from '../.config'; + +// const TMP_PATH = path.join(__dirname, '../../tmp'); +// const LOCAL_PATH = path.join(__dirname, '../../consumer'); +// const REMOTE_PATH = '/home/litlyx/consumer'; +// const ZIP_NAME = 'consumer.zip'; + +// const MODE = DeployHelper.getMode(); +// const SKIP_BUILD = DeployHelper.getArgAt(0) == '--no-build'; + +// console.log('Deploying consumer in mode:', MODE); + +// setTimeout(() => { main(); }, 3000); + +// async function main() { + +// if (fs.existsSync(TMP_PATH)) fs.rmSync(TMP_PATH, { force: true, recursive: true }); +// fs.ensureDirSync(TMP_PATH); + + +// if (!SKIP_BUILD) { +// console.log('Building'); +// child.execSync(`cd ${LOCAL_PATH} && pnpm run build`); +// } + + +// console.log('Creting zip file'); +// const archive = createZip(TMP_PATH + '/' + ZIP_NAME); +// archive.directory(LOCAL_PATH + '/dist', '/dist'); + +// if (MODE === 'testmode') { +// const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8'); +// const REDIS_URL = ecosystemContent.match(/REDIS_URL: ["'](.*?)["']/)[1]; +// const devContent = ecosystemContent +// .replace(REDIS_URL, `redis://${REMOTE_HOST_TESTMODE}`) +// .replace(DATABASE_CONNECTION_STRING_PRODUCTION, `redis://${DATABASE_CONNECTION_STRING_TESTMODE}`); +// archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' }); +// } else { +// archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' }) +// } + + +// archive.file(LOCAL_PATH + '/package.json', { name: '/package.json' }); +// archive.file(LOCAL_PATH + '/pnpm-lock.yaml', { name: '/pnpm-lock.yaml' }); +// await archive.finalize(); + +// await DeployHelper.connect(); + +// const { scp, ssh } = DeployHelper.instances(); + +// console.log('Creating remote structure'); +// console.log('Check existing'); +// const remoteExist = await scp.exists(REMOTE_PATH); +// console.log('Exist', remoteExist); +// if (remoteExist) { +// console.log('Deleting'); +// await DeployHelper.execute(`rm -r ${REMOTE_PATH}`); +// } + +// console.log('Creating folder'); +// await scp.mkdir(REMOTE_PATH); + +// console.log('Uploading zip file'); +// await scp.uploadFile(TMP_PATH + '/' + ZIP_NAME, REMOTE_PATH + '/' + ZIP_NAME); +// scp.close(); + +// console.log('Cleaning local'); +// fs.rmSync(TMP_PATH + '/' + ZIP_NAME, { force: true, recursive: true }); + +// console.log('Extracting remote'); +// await DeployHelper.execute(`cd ${REMOTE_PATH} && unzip ${ZIP_NAME} && rm -r ${ZIP_NAME}`); + +// console.log('Installing remote'); +// await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pnpm i`); + +// console.log('Executing remote'); +// await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pm2 start ecosystem.config.js`); + +// ssh.dispose(); + +// } diff --git a/scripts/payments/shared.ts b/scripts/payments/shared.ts new file mode 100644 index 0000000..ebdd3ab --- /dev/null +++ b/scripts/payments/shared.ts @@ -0,0 +1,23 @@ + +import { SharedHelper } from "../helpers/shared-helper"; +import path from "node:path"; + +const helper = new SharedHelper(path.join(__dirname, '../../payments/src/shared')) + +helper.clear(); + +helper.create('utils'); +helper.copy('utils/requireEnv.ts'); + +helper.create('services'); +helper.copy('services/DatabaseService.ts'); +helper.copy('services/EmailService.ts'); + +helper.create('schema'); +helper.copy('schema/UserSchema.ts'); + +helper.create('schema/project'); +helper.copy('schema/project/ProjectSchema.ts'); + +helper.create('data'); +helper.copy('data/PLANS.ts'); \ No newline at end of file diff --git a/shared_global/data/PLANS.ts b/shared_global/data/PLANS.ts new file mode 100644 index 0000000..eacdbf6 --- /dev/null +++ b/shared_global/data/PLANS.ts @@ -0,0 +1,201 @@ +export type PREMIUM_TAG = typeof PREMIUM_TAGS[number]; + +export const PREMIUM_TAGS = [ + 'FREE', + 'PLAN_1', + 'PLAN_2', + 'CUSTOM_1', + 'INCUBATION', + 'ACCELERATION', + 'GROWTH', + 'EXPANSION', + 'SCALING', + 'UNICORN', + 'LIFETIME_GROWTH_ONETIME', + 'GROWTH_DUMMY', + 'APPSUMO_INCUBATION', + 'APPSUMO_ACCELERATION', + 'APPSUMO_GROWTH', + 'APPSUMO_UNICORN' +] as const; + + +export type PREMIUM_DATA = { + COUNT_LIMIT: number, + AI_MESSAGE_LIMIT: number, + PRICE: string, + PRICE_TEST: string, + ID: number, + COST: number, + TAG: PREMIUM_TAG +} + +export const PREMIUM_PLAN: Record = { + FREE: { + ID: 0, + COUNT_LIMIT: 5_000, + AI_MESSAGE_LIMIT: 10, + PRICE: 'price_1POKCMB2lPUiVs9VLe3QjIHl', + PRICE_TEST: 'price_1PNbHYB2lPUiVs9VZP32xglF', + COST: 0, + TAG: 'FREE' + }, + PLAN_1: { + ID: 1, + COUNT_LIMIT: 150_000, + AI_MESSAGE_LIMIT: 100, + PRICE: 'price_1POKCOB2lPUiVs9VC13s2rQw', + PRICE_TEST: 'price_1PNZjVB2lPUiVs9VrsTbJL04', + COST: 0, + TAG: 'PLAN_1' + }, + PLAN_2: { + ID: 2, + COUNT_LIMIT: 500_000, + AI_MESSAGE_LIMIT: 5_000, + PRICE: 'price_1POKCKB2lPUiVs9Vol8XOmhW', + PRICE_TEST: 'price_1POK34B2lPUiVs9VIROb0IIV', + COST: 0, + TAG: 'PLAN_2' + }, + CUSTOM_1: { + ID: 1001, + COUNT_LIMIT: 10_000_000, + AI_MESSAGE_LIMIT: 100_000, + PRICE: 'price_1POKZyB2lPUiVs9VMAY6jXTV', + PRICE_TEST: '', + COST: 0, + TAG: 'CUSTOM_1' + }, + INCUBATION: { + ID: 101, + COUNT_LIMIT: 50_000, + AI_MESSAGE_LIMIT: 30, + PRICE: 'price_1PdsyzB2lPUiVs9V4J246Jw0', + PRICE_TEST: '', + COST: 499, + TAG: 'INCUBATION' + }, + ACCELERATION: { + ID: 102, + COUNT_LIMIT: 150_000, + AI_MESSAGE_LIMIT: 100, + PRICE: 'price_1Pdt5bB2lPUiVs9VhkuCouEt', + PRICE_TEST: '', + COST: 999, + TAG: 'ACCELERATION' + }, + GROWTH: { + ID: 103, + COUNT_LIMIT: 500_000, + AI_MESSAGE_LIMIT: 3_000, + PRICE: 'price_1PdszrB2lPUiVs9VIdkT3thv', + PRICE_TEST: '', + COST: 2999, + TAG: 'GROWTH' + }, + EXPANSION: { + ID: 104, + COUNT_LIMIT: 1_000_000, + AI_MESSAGE_LIMIT: 5_000, + PRICE: 'price_1Pdt0xB2lPUiVs9V0Rdt80Fe', + PRICE_TEST: '', + COST: 5999, + TAG: 'EXPANSION' + }, + SCALING: { + ID: 105, + COUNT_LIMIT: 2_500_000, + AI_MESSAGE_LIMIT: 10_000, + PRICE: 'price_1Pdt1UB2lPUiVs9VUmxntSwZ', + PRICE_TEST: '', + COST: 9999, + TAG: 'SCALING' + }, + UNICORN: { + ID: 106, + COUNT_LIMIT: 5_000_000, + AI_MESSAGE_LIMIT: 20_000, + PRICE: 'price_1Pdt2LB2lPUiVs9VGBFAIG9G', + PRICE_TEST: '', + COST: 14999, + TAG: 'UNICORN' + }, + LIFETIME_GROWTH_ONETIME: { + ID: 2001, + COUNT_LIMIT: 500_000, + AI_MESSAGE_LIMIT: 3_000, + PRICE: 'price_1PvewGB2lPUiVs9VLheJC8s1', + PRICE_TEST: 'price_1Pvf7LB2lPUiVs9VMFNyzpim', + COST: 239900, + TAG: 'LIFETIME_GROWTH_ONETIME' + }, + GROWTH_DUMMY: { + ID: 5001, + COUNT_LIMIT: 500_000, + AI_MESSAGE_LIMIT: 3_000, + PRICE: 'price_1PvgoRB2lPUiVs9VC51YBT7J', + PRICE_TEST: 'price_1PvgRTB2lPUiVs9V3kFSNC3G', + COST: 0, + TAG: 'GROWTH_DUMMY' + }, + APPSUMO_INCUBATION: { + ID: 6001, + COUNT_LIMIT: 50_000, + AI_MESSAGE_LIMIT: 30, + PRICE: 'price_1QIXwbB2lPUiVs9VKSsoksaU', + PRICE_TEST: '', + COST: 0, + TAG: 'APPSUMO_INCUBATION' + }, + APPSUMO_ACCELERATION: { + ID: 6002, + COUNT_LIMIT: 150_000, + AI_MESSAGE_LIMIT: 100, + PRICE: 'price_1QIXxRB2lPUiVs9VrjaVRoOl', + PRICE_TEST: '', + COST: 0, + TAG: 'APPSUMO_ACCELERATION' + }, + APPSUMO_GROWTH: { + ID: 6003, + COUNT_LIMIT: 500_000, + AI_MESSAGE_LIMIT: 3_000, + PRICE: 'price_1QIXy8B2lPUiVs9VQBOUPAoE', + PRICE_TEST: '', + COST: 0, + TAG: 'APPSUMO_GROWTH' + }, + APPSUMO_UNICORN: { + ID: 6006, + COUNT_LIMIT: 5_000_000, + AI_MESSAGE_LIMIT: 20_000, + PRICE: 'price_1Qls1lB2lPUiVs9VI6ej8hwE', + PRICE_TEST: '', + COST: 0, + TAG: 'APPSUMO_UNICORN' + } +} + +export function getPlanFromTag(tag: PREMIUM_TAG) { + return PREMIUM_PLAN[tag]; +} + +export function getPlanFromId(id: number) { + for (const tag of PREMIUM_TAGS) { + const plan = getPlanFromTag(tag); + if (plan.ID === id) return plan; + } +} + +export function getPlanFromPrice(price: string, testMode: boolean) { + for (const tag of PREMIUM_TAGS) { + const plan = getPlanFromTag(tag); + if (testMode) { + if (plan.PRICE_TEST === price) return plan; + } else { + if (plan.PRICE === price) return plan; + } + + } +} \ No newline at end of file