feat: implement payment service with error handling and validation
Add new payment service with repository, error handling, and validation Add exceptions package with database and validation utilities Update database package with new schema and zod types Add turbo start script for development
This commit is contained in:
0
apps/bot/src/handlers/purchase-subscription.ts
Normal file
0
apps/bot/src/handlers/purchase-subscription.ts
Normal file
201
package-lock.json
generated
201
package-lock.json
generated
@@ -594,7 +594,6 @@
|
|||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.11.1.tgz",
|
||||||
"integrity": "sha512-z6rCTQN741wxDq82cpdzx2uVykpnQIXalLhrWQSR0jlBVOxCIkz3HZnd8ern3uYTcWKfB3IpVAF7K2FU8t/8AQ==",
|
"integrity": "sha512-z6rCTQN741wxDq82cpdzx2uVykpnQIXalLhrWQSR0jlBVOxCIkz3HZnd8ern3uYTcWKfB3IpVAF7K2FU8t/8AQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jiti": "2.4.2"
|
"jiti": "2.4.2"
|
||||||
@@ -604,14 +603,18 @@
|
|||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.11.1.tgz",
|
||||||
"integrity": "sha512-lWRb/YSWu8l4Yum1UXfGLtqFzZkVS2ygkWYpgkbgMHn9XJlMITIgeMvJyX5GepChzhmxuSuiq/MY/kGFweOpGw==",
|
"integrity": "sha512-lWRb/YSWu8l4Yum1UXfGLtqFzZkVS2ygkWYpgkbgMHn9XJlMITIgeMvJyX5GepChzhmxuSuiq/MY/kGFweOpGw==",
|
||||||
"devOptional": true,
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/dmmf": {
|
||||||
|
"version": "6.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/dmmf/-/dmmf-6.11.1.tgz",
|
||||||
|
"integrity": "sha512-EwPSlhz7Qt5msepoxlKIGjtGP6fUEFQyxrYx6f0IdvQ8l0rLSJEqBrTiMpNhgKQFeR8kuiQTv+xzHk/l8mV+Xw==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.11.1.tgz",
|
||||||
"integrity": "sha512-6eKEcV6V8W2eZAUwX2xTktxqPM4vnx3sxz3SDtpZwjHKpC6lhOtc4vtAtFUuf5+eEqBk+dbJ9Dcaj6uQU+FNNg==",
|
"integrity": "sha512-6eKEcV6V8W2eZAUwX2xTktxqPM4vnx3sxz3SDtpZwjHKpC6lhOtc4vtAtFUuf5+eEqBk+dbJ9Dcaj6uQU+FNNg==",
|
||||||
"devOptional": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -625,14 +628,12 @@
|
|||||||
"version": "6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9",
|
"version": "6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.11.1-1.f40f79ec31188888a2e33acda0ecc8fd10a853a9.tgz",
|
||||||
"integrity": "sha512-swFJTOOg4tHyOM1zB/pHb3MeH0i6t7jFKn5l+ZsB23d9AQACuIRo9MouvuKGvnDogzkcjbWnXi/NvOZ0+n5Jfw==",
|
"integrity": "sha512-swFJTOOg4tHyOM1zB/pHb3MeH0i6t7jFKn5l+ZsB23d9AQACuIRo9MouvuKGvnDogzkcjbWnXi/NvOZ0+n5Jfw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.11.1.tgz",
|
||||||
"integrity": "sha512-NBYzmkXTkj9+LxNPRSndaAeALOL1Gr3tjvgRYNqruIPlZ6/ixLeuE/5boYOewant58tnaYFZ5Ne0jFBPfGXHpQ==",
|
"integrity": "sha512-NBYzmkXTkj9+LxNPRSndaAeALOL1Gr3tjvgRYNqruIPlZ6/ixLeuE/5boYOewant58tnaYFZ5Ne0jFBPfGXHpQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.11.1",
|
"@prisma/debug": "6.11.1",
|
||||||
@@ -640,11 +641,27 @@
|
|||||||
"@prisma/get-platform": "6.11.1"
|
"@prisma/get-platform": "6.11.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@prisma/generator": {
|
||||||
|
"version": "6.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/generator/-/generator-6.11.1.tgz",
|
||||||
|
"integrity": "sha512-LNpsWyVjebWbVIHVw0bTTQUIlnK+92naGB5fWnjEJqJcxXuDnNQ0XomZAQ1BrqwbMrYL6RYS4whkTpKtu1ijLQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/generator-helper": {
|
||||||
|
"version": "6.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.11.1.tgz",
|
||||||
|
"integrity": "sha512-mxdeBqW8inKkuE2nHLo0SsdzsjY51cyRQYgD1uEshvy1/n5eXJJOhEA9T948qA0ZBYUb8SppO2HZtQjwkGwzjQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "6.11.1",
|
||||||
|
"@prisma/dmmf": "6.11.1",
|
||||||
|
"@prisma/generator": "6.11.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@prisma/get-platform": {
|
"node_modules/@prisma/get-platform": {
|
||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.11.1.tgz",
|
||||||
"integrity": "sha512-b2Z8oV2gwvdCkFemBTFd0x4lsL4O2jLSx8lB7D+XqoFALOQZPa7eAPE1NU0Mj1V8gPHRxIsHnyUNtw2i92psUw==",
|
"integrity": "sha512-b2Z8oV2gwvdCkFemBTFd0x4lsL4O2jLSx8lB7D+XqoFALOQZPa7eAPE1NU0Mj1V8gPHRxIsHnyUNtw2i92psUw==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/debug": "6.11.1"
|
"@prisma/debug": "6.11.1"
|
||||||
@@ -662,14 +679,39 @@
|
|||||||
"resolved": "packages/eslint-config",
|
"resolved": "packages/eslint-config",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@repo/exceptions": {
|
||||||
|
"resolved": "packages/exceptions",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@repo/logger": {
|
"node_modules/@repo/logger": {
|
||||||
"resolved": "packages/logger",
|
"resolved": "packages/logger",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@repo/services": {
|
||||||
|
"resolved": "packages/services",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@repo/typescript-config": {
|
"node_modules/@repo/typescript-config": {
|
||||||
"resolved": "packages/typescript-config",
|
"resolved": "packages/typescript-config",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@repo/utils": {
|
||||||
|
"resolved": "packages/utils",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
|
"version": "4.44.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.2.tgz",
|
||||||
|
"integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/@tsconfig/node10": {
|
"node_modules/@tsconfig/node10": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||||
@@ -1490,6 +1532,12 @@
|
|||||||
"node": ">=0.8"
|
"node": ">=0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/code-block-writer": {
|
||||||
|
"version": "12.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz",
|
||||||
|
"integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
@@ -3126,7 +3174,6 @@
|
|||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
|
||||||
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
"integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
@@ -3241,6 +3288,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@@ -3351,6 +3404,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/neverthrow": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/neverthrow/-/neverthrow-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "^4.24.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch": {
|
"node_modules/node-fetch": {
|
||||||
"version": "2.7.0",
|
"version": "2.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
@@ -3686,7 +3751,6 @@
|
|||||||
"version": "6.11.1",
|
"version": "6.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.11.1.tgz",
|
||||||
"integrity": "sha512-VzJToRlV0s9Vu2bfqHiRJw73hZNCG/AyJeX+kopbu4GATTjTUdEWUteO3p4BLYoHpMS4o8pD3v6tF44BHNZI1w==",
|
"integrity": "sha512-VzJToRlV0s9Vu2bfqHiRJw73hZNCG/AyJeX+kopbu4GATTjTUdEWUteO3p4BLYoHpMS4o8pD3v6tF44BHNZI1w==",
|
||||||
"devOptional": true,
|
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3708,6 +3772,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prisma-json-types-generator": {
|
||||||
|
"version": "3.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prisma-json-types-generator/-/prisma-json-types-generator-3.5.1.tgz",
|
||||||
|
"integrity": "sha512-PKhsAxs6IYx/BD6vomsfm0SZQ3wSUDb3GKof7o/CaAJ7kCLmHub6/qJQfD+qwve3omoSrp9kQ29Qj2T980PEUA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/generator-helper": "^6.11.0",
|
||||||
|
"semver": "^7.7.2",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prisma-json-types-generator": "index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/arthurfiorette/prisma-json-types-generator?sponsor=1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@prisma/client": "~6.11",
|
||||||
|
"prisma": "~6.11",
|
||||||
|
"typescript": "^5.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prisma-json-types-generator/node_modules/semver": {
|
||||||
|
"version": "7.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||||
|
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prop-types": {
|
"node_modules/prop-types": {
|
||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
@@ -4452,6 +4553,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/turbo": {
|
"node_modules/turbo": {
|
||||||
"version": "2.5.4",
|
"version": "2.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/turbo/-/turbo-2.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/turbo/-/turbo-2.5.4.tgz",
|
||||||
@@ -4649,7 +4756,6 @@
|
|||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -4938,6 +5044,25 @@
|
|||||||
"zod": "^3.21.4"
|
"zod": "^3.21.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/zod-prisma-types": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod-prisma-types/-/zod-prisma-types-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-S4spVBMJAmecLv+aLyRhXK26qW9nWwcsOf1H1fRcEmiI8DPbftZ99u0fhqHlymuTpmMcUpuKxcNNOIqNY0ScSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/generator-helper": "^6.3.0",
|
||||||
|
"code-block-writer": "^12.0.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"zod": "^3.24.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"zod-prisma-types": "dist/bin.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@prisma/client": "^4.x.x || ^5.x.x || ^6.x.x",
|
||||||
|
"prisma": "^4.x.x || ^5.x.x || ^6.x.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/config": {
|
"packages/config": {
|
||||||
"name": "@repo/config",
|
"name": "@repo/config",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
@@ -4954,7 +5079,10 @@
|
|||||||
"name": "@repo/db",
|
"name": "@repo/db",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.11.1"
|
"@prisma/client": "^6.11.1",
|
||||||
|
"@repo/exceptions": "*",
|
||||||
|
"prisma-json-types-generator": "^3.5.1",
|
||||||
|
"zod-prisma-types": "^3.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.0.10",
|
"@types/node": "^24.0.10",
|
||||||
@@ -4978,6 +5106,27 @@
|
|||||||
"typescript-eslint": "^8.35.0"
|
"typescript-eslint": "^8.35.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/exceptions": {
|
||||||
|
"name": "@repo/exceptions",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"neverthrow": "^8.2.0",
|
||||||
|
"zod": "^4.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/typescript-config": "*",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/exceptions/node_modules/zod": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-X2niJNY54MGam4L6Kj0AxeedeDIi/E5QFW0On2faSX5J4/pfLk1tW+cRMIMoojnCavn/u5W/kX17e1CSGnKMxA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/logger": {
|
"packages/logger": {
|
||||||
"name": "@repo/logger",
|
"name": "@repo/logger",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -4985,10 +5134,42 @@
|
|||||||
"typescript": "latest"
|
"typescript": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"packages/services": {
|
||||||
|
"name": "@repo/services",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@repo/db": "*",
|
||||||
|
"@repo/exceptions": "*",
|
||||||
|
"@repo/utils": "*",
|
||||||
|
"neverthrow": "^8.2.0",
|
||||||
|
"zod": "^4.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/typescript-config": "*",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packages/services/node_modules/zod": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-X2niJNY54MGam4L6Kj0AxeedeDIi/E5QFW0On2faSX5J4/pfLk1tW+cRMIMoojnCavn/u5W/kX17e1CSGnKMxA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
},
|
||||||
"packages/typescript-config": {
|
"packages/typescript-config": {
|
||||||
"name": "@repo/typescript-config",
|
"name": "@repo/typescript-config",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"packages/utils": {
|
||||||
|
"name": "@repo/utils",
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/exceptions": "*",
|
||||||
|
"@repo/typescript-config": "*",
|
||||||
|
"typescript": "latest"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
192
packages/database/generated/zod/index.ts
Normal file
192
packages/database/generated/zod/index.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
import { Prisma } from '../prisma';
|
||||||
|
|
||||||
|
/////////////////////////////////////////
|
||||||
|
// HELPER FUNCTIONS
|
||||||
|
/////////////////////////////////////////
|
||||||
|
|
||||||
|
// JSON
|
||||||
|
//------------------------------------------------------
|
||||||
|
|
||||||
|
export type NullableJsonInput = Prisma.JsonValue | null | 'JsonNull' | 'DbNull' | Prisma.NullTypes.DbNull | Prisma.NullTypes.JsonNull;
|
||||||
|
|
||||||
|
export const transformJsonNull = (v?: NullableJsonInput) => {
|
||||||
|
if (!v || v === 'DbNull') return Prisma.DbNull;
|
||||||
|
if (v === 'JsonNull') return Prisma.JsonNull;
|
||||||
|
return v;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const JsonValueSchema: z.ZodType<Prisma.JsonValue> = z.lazy(() =>
|
||||||
|
z.union([
|
||||||
|
z.string(),
|
||||||
|
z.number(),
|
||||||
|
z.boolean(),
|
||||||
|
z.literal(null),
|
||||||
|
z.record(z.lazy(() => JsonValueSchema.optional())),
|
||||||
|
z.array(z.lazy(() => JsonValueSchema)),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export type JsonValueType = z.infer<typeof JsonValueSchema>;
|
||||||
|
|
||||||
|
export const NullableJsonValue = z
|
||||||
|
.union([JsonValueSchema, z.literal('DbNull'), z.literal('JsonNull')])
|
||||||
|
.nullable()
|
||||||
|
.transform((v) => transformJsonNull(v));
|
||||||
|
|
||||||
|
export type NullableJsonValueType = z.infer<typeof NullableJsonValue>;
|
||||||
|
|
||||||
|
export const InputJsonValueSchema: z.ZodType<Prisma.InputJsonValue> = z.lazy(() =>
|
||||||
|
z.union([
|
||||||
|
z.string(),
|
||||||
|
z.number(),
|
||||||
|
z.boolean(),
|
||||||
|
z.object({ toJSON: z.function(z.tuple([]), z.any()) }),
|
||||||
|
z.record(z.lazy(() => z.union([InputJsonValueSchema, z.literal(null)]))),
|
||||||
|
z.array(z.lazy(() => z.union([InputJsonValueSchema, z.literal(null)]))),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
export type InputJsonValueType = z.infer<typeof InputJsonValueSchema>;
|
||||||
|
|
||||||
|
// DECIMAL
|
||||||
|
//------------------------------------------------------
|
||||||
|
|
||||||
|
export const DecimalJsLikeSchema: z.ZodType<Prisma.DecimalJsLike> = z.object({
|
||||||
|
d: z.array(z.number()),
|
||||||
|
e: z.number(),
|
||||||
|
s: z.number(),
|
||||||
|
toFixed: z.function(z.tuple([]), z.string()),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const DECIMAL_STRING_REGEX = /^(?:-?Infinity|NaN|-?(?:0[bB][01]+(?:\.[01]+)?(?:[pP][-+]?\d+)?|0[oO][0-7]+(?:\.[0-7]+)?(?:[pP][-+]?\d+)?|0[xX][\da-fA-F]+(?:\.[\da-fA-F]+)?(?:[pP][-+]?\d+)?|(?:\d+|\d*\.\d+)(?:[eE][-+]?\d+)?))$/;
|
||||||
|
|
||||||
|
export const isValidDecimalInput =
|
||||||
|
(v?: null | string | number | Prisma.DecimalJsLike): v is string | number | Prisma.DecimalJsLike => {
|
||||||
|
if (v === undefined || v === null) return false;
|
||||||
|
return (
|
||||||
|
(typeof v === 'object' && 'd' in v && 'e' in v && 's' in v && 'toFixed' in v) ||
|
||||||
|
(typeof v === 'string' && DECIMAL_STRING_REGEX.test(v)) ||
|
||||||
|
typeof v === 'number'
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
/////////////////////////////////////////
|
||||||
|
// ENUMS
|
||||||
|
/////////////////////////////////////////
|
||||||
|
|
||||||
|
export const TransactionIsolationLevelSchema = z.enum(['ReadUncommitted','ReadCommitted','RepeatableRead','Serializable']);
|
||||||
|
|
||||||
|
export const TelegramSessionScalarFieldEnumSchema = z.enum(['id','data','createdAt','updatedAt']);
|
||||||
|
|
||||||
|
export const TelegramUserScalarFieldEnumSchema = z.enum(['id','username','isBlocked','firstName','lastName','languageCode','isPremium','createdAt','updatedAt']);
|
||||||
|
|
||||||
|
export const PaymentScalarFieldEnumSchema = z.enum(['id','telegramUserId','amount','currency','status','provider','providerId','metadata','createdAt','updatedAt']);
|
||||||
|
|
||||||
|
export const PlanScalarFieldEnumSchema = z.enum(['id','code','name','description','durationDays','dailyGenerations','price','currency']);
|
||||||
|
|
||||||
|
export const SubscriptionScalarFieldEnumSchema = z.enum(['id','telegramUserId','planId','startedAt','expiresAt','canceledAt','createdAt','updatedAt']);
|
||||||
|
|
||||||
|
export const SortOrderSchema = z.enum(['asc','desc']);
|
||||||
|
|
||||||
|
export const JsonNullValueInputSchema = z.enum(['JsonNull',]).transform((value) => (value === 'JsonNull' ? Prisma.JsonNull : value));
|
||||||
|
|
||||||
|
export const NullableJsonNullValueInputSchema = z.enum(['DbNull','JsonNull',]).transform((value) => value === 'JsonNull' ? Prisma.JsonNull : value === 'DbNull' ? Prisma.DbNull : value);
|
||||||
|
|
||||||
|
export const QueryModeSchema = z.enum(['default','insensitive']);
|
||||||
|
|
||||||
|
export const JsonNullValueFilterSchema = z.enum(['DbNull','JsonNull','AnyNull',]).transform((value) => value === 'JsonNull' ? Prisma.JsonNull : value === 'DbNull' ? Prisma.JsonNull : value === 'AnyNull' ? Prisma.AnyNull : value);
|
||||||
|
|
||||||
|
export const NullsOrderSchema = z.enum(['first','last']);
|
||||||
|
|
||||||
|
export const PaymentStatusSchema = z.enum(['PENDING','COMPLETED','FAILED','REFUNDED']);
|
||||||
|
|
||||||
|
export type PaymentStatusType = `${z.infer<typeof PaymentStatusSchema>}`
|
||||||
|
|
||||||
|
/////////////////////////////////////////
|
||||||
|
// MODELS
|
||||||
|
/////////////////////////////////////////
|
||||||
|
|
||||||
|
/////////////////////////////////////////
|
||||||
|
// TELEGRAM SESSION SCHEMA
|
||||||
|
/////////////////////////////////////////
|
||||||
|
|
||||||
|
export const TelegramSessionSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
data: JsonValueSchema,
|
||||||
|
createdAt: z.coerce.date(),
|
||||||
|
updatedAt: z.coerce.date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type TelegramSession = z.infer<typeof TelegramSessionSchema>
|
||||||
|
|
||||||
|
/////////////////////////////////////////
|
||||||
|
// TELEGRAM USER SCHEMA
|
||||||
|
/////////////////////////////////////////
|
||||||
|
|
||||||
|
export const TelegramUserSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
username: z.string().nullable(),
|
||||||
|
isBlocked: z.boolean(),
|
||||||
|
firstName: z.string().nullable(),
|
||||||
|
lastName: z.string().nullable(),
|
||||||
|
languageCode: z.string().nullable(),
|
||||||
|
isPremium: z.boolean().nullable(),
|
||||||
|
createdAt: z.coerce.date(),
|
||||||
|
updatedAt: z.coerce.date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type TelegramUser = z.infer<typeof TelegramUserSchema>
|
||||||
|
|
||||||
|
/////////////////////////////////////////
|
||||||
|
// PAYMENT SCHEMA
|
||||||
|
/////////////////////////////////////////
|
||||||
|
|
||||||
|
export const PaymentSchema = z.object({
|
||||||
|
status: PaymentStatusSchema,
|
||||||
|
id: z.string().cuid(),
|
||||||
|
telegramUserId: z.string(),
|
||||||
|
amount: z.instanceof(Prisma.Decimal, { message: "Field 'amount' must be a Decimal. Location: ['Models', 'Payment']"}),
|
||||||
|
currency: z.string(),
|
||||||
|
provider: z.string(),
|
||||||
|
providerId: z.string().nullable(),
|
||||||
|
metadata: JsonValueSchema.nullable(),
|
||||||
|
createdAt: z.coerce.date(),
|
||||||
|
updatedAt: z.coerce.date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Payment = z.infer<typeof PaymentSchema>
|
||||||
|
|
||||||
|
/////////////////////////////////////////
|
||||||
|
// PLAN SCHEMA
|
||||||
|
/////////////////////////////////////////
|
||||||
|
|
||||||
|
export const PlanSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
code: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string().nullable(),
|
||||||
|
durationDays: z.number().int(),
|
||||||
|
dailyGenerations: z.number().int(),
|
||||||
|
price: z.number().int(),
|
||||||
|
currency: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Plan = z.infer<typeof PlanSchema>
|
||||||
|
|
||||||
|
/////////////////////////////////////////
|
||||||
|
// SUBSCRIPTION SCHEMA
|
||||||
|
/////////////////////////////////////////
|
||||||
|
|
||||||
|
export const SubscriptionSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
telegramUserId: z.string(),
|
||||||
|
planId: z.string(),
|
||||||
|
startedAt: z.coerce.date(),
|
||||||
|
expiresAt: z.coerce.date(),
|
||||||
|
canceledAt: z.coerce.date().nullable(),
|
||||||
|
createdAt: z.coerce.date(),
|
||||||
|
updatedAt: z.coerce.date(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Subscription = z.infer<typeof SubscriptionSchema>
|
||||||
@@ -8,7 +8,10 @@
|
|||||||
"db:push": "prisma db push"
|
"db:push": "prisma db push"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.11.1"
|
"@prisma/client": "^6.11.1",
|
||||||
|
"@repo/exceptions": "*",
|
||||||
|
"prisma-json-types-generator": "^3.5.1",
|
||||||
|
"zod-prisma-types": "^3.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.0.10",
|
"@types/node": "^24.0.10",
|
||||||
|
|||||||
@@ -9,6 +9,22 @@ generator client {
|
|||||||
output = "../generated/prisma"
|
output = "../generated/prisma"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generator zod {
|
||||||
|
provider = "zod-prisma-types"
|
||||||
|
output = "../generated/zod"
|
||||||
|
writeBarrelFiles = false // default is true
|
||||||
|
createInputTypes = false // default is true
|
||||||
|
createModelTypes = true // default is true
|
||||||
|
addInputTypeValidation = false // default is true
|
||||||
|
addIncludeType = false // default is true
|
||||||
|
addSelectType = false // default is true
|
||||||
|
validateWhereUniqueInput = false // default is true
|
||||||
|
}
|
||||||
|
|
||||||
|
generator json {
|
||||||
|
provider = "prisma-json-types-generator"
|
||||||
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
@@ -31,4 +47,62 @@ model TelegramUser {
|
|||||||
isPremium Boolean?
|
isPremium Boolean?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
payments Payment[]
|
||||||
|
subscription Subscription?
|
||||||
|
}
|
||||||
|
|
||||||
|
model Payment {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
telegramUserId String
|
||||||
|
amount Decimal @db.Decimal(10, 2)
|
||||||
|
currency String
|
||||||
|
status PaymentStatus @default(PENDING)
|
||||||
|
provider String
|
||||||
|
providerId String?
|
||||||
|
metadata Json?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
telegramUser TelegramUser @relation(fields: [telegramUserId], references: [id])
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PaymentStatus {
|
||||||
|
PENDING
|
||||||
|
COMPLETED
|
||||||
|
FAILED
|
||||||
|
REFUNDED
|
||||||
|
}
|
||||||
|
|
||||||
|
model Plan {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
code String @unique()
|
||||||
|
name String
|
||||||
|
description String?
|
||||||
|
durationDays Int
|
||||||
|
dailyGenerations Int
|
||||||
|
price Int
|
||||||
|
currency String
|
||||||
|
|
||||||
|
subscriptions Subscription[]
|
||||||
|
|
||||||
|
@@index([code])
|
||||||
|
}
|
||||||
|
|
||||||
|
model Subscription {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
telegramUserId String @unique
|
||||||
|
planId String
|
||||||
|
|
||||||
|
startedAt DateTime @default(now())
|
||||||
|
expiresAt DateTime
|
||||||
|
canceledAt DateTime?
|
||||||
|
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
plan Plan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
||||||
|
telegramUser TelegramUser @relation(fields: [telegramUserId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@index([telegramUserId])
|
||||||
|
@@index([planId])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,3 +16,7 @@ export const prisma = new PrismaClientBase().$extends({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type PrismaClient = typeof prisma;
|
export type PrismaClient = typeof prisma;
|
||||||
|
|
||||||
|
export type PrismaClientArg =
|
||||||
|
| Parameters<Parameters<PrismaClient['$transaction']>[0]>[0]
|
||||||
|
| PrismaClient;
|
||||||
|
|||||||
@@ -1,2 +1,7 @@
|
|||||||
export { prisma } from "./client"; // exports instance of prisma
|
export { prisma, type PrismaClientArg, type PrismaClient } from './client';
|
||||||
export * from "../generated/prisma"; // exports generated types from prisma
|
export * from '../generated/prisma';
|
||||||
|
export {
|
||||||
|
PaymentSchema,
|
||||||
|
TelegramUserSchema,
|
||||||
|
TelegramSessionSchema,
|
||||||
|
} from '../generated/zod';
|
||||||
|
|||||||
17
packages/exceptions/package.json
Normal file
17
packages/exceptions/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/exceptions",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"neverthrow": "^8.2.0",
|
||||||
|
"zod": "^4.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/typescript-config": "*",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
packages/exceptions/src/constructors.ts
Normal file
35
packages/exceptions/src/constructors.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { AppError, DatabaseError, ErrorCode, ValidationError } from './types';
|
||||||
|
|
||||||
|
export const createAppError = <T>(
|
||||||
|
code: T,
|
||||||
|
message: string,
|
||||||
|
details?: Record<string, unknown>,
|
||||||
|
cause?: Error
|
||||||
|
): AppError<T> => ({
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
details,
|
||||||
|
cause,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const createValidationError = (
|
||||||
|
field: string,
|
||||||
|
value: unknown,
|
||||||
|
message: string
|
||||||
|
): ValidationError => ({
|
||||||
|
code: ErrorCode.VALIDATION_ERROR,
|
||||||
|
message,
|
||||||
|
field,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const databaseError = (
|
||||||
|
operation: string,
|
||||||
|
cause?: Error
|
||||||
|
): DatabaseError =>
|
||||||
|
createAppError(
|
||||||
|
ErrorCode.DATABASE_ERROR,
|
||||||
|
`Database error during ${operation}`,
|
||||||
|
undefined,
|
||||||
|
cause
|
||||||
|
);
|
||||||
3
packages/exceptions/src/index.ts
Normal file
3
packages/exceptions/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './types';
|
||||||
|
export * from './constructors';
|
||||||
|
export * from './utils';
|
||||||
21
packages/exceptions/src/types.ts
Normal file
21
packages/exceptions/src/types.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export enum ErrorCode {
|
||||||
|
// System errors
|
||||||
|
DATABASE_ERROR = 'DATABASE_ERROR',
|
||||||
|
VALIDATION_ERROR = 'VALIDATION_ERROR',
|
||||||
|
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AppError<T> = {
|
||||||
|
readonly code: T;
|
||||||
|
readonly message: string;
|
||||||
|
readonly details?: Record<string, unknown>;
|
||||||
|
readonly cause?: Error;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ValidationError = AppError<ErrorCode.VALIDATION_ERROR> & {
|
||||||
|
readonly code: ErrorCode.VALIDATION_ERROR;
|
||||||
|
readonly field: string;
|
||||||
|
readonly value: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DatabaseError = AppError<ErrorCode.DATABASE_ERROR>;
|
||||||
25
packages/exceptions/src/utils/database.ts
Normal file
25
packages/exceptions/src/utils/database.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { ResultAsync, err, ok } from 'neverthrow';
|
||||||
|
import { AppError } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a database operation in a Result type to handle errors gracefully
|
||||||
|
* @param operation - The database operation to execute
|
||||||
|
* @param errorFactory - Function to create a custom error if the operation fails
|
||||||
|
* @returns Result containing the operation result or error
|
||||||
|
*/
|
||||||
|
export const wrapDbOperation = <T, E extends AppError<unknown>>(
|
||||||
|
operation: () => Promise<T>,
|
||||||
|
errorFactory: (error: unknown) => E
|
||||||
|
): ResultAsync<T, E> => {
|
||||||
|
return ResultAsync.fromPromise(operation(), errorFactory);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const wrapDbOperationWithNull = <T, E extends AppError<unknown>>(
|
||||||
|
operation: () => Promise<T | null>,
|
||||||
|
notFoundError: E,
|
||||||
|
dbErrorFactory: (error: unknown) => E
|
||||||
|
): ResultAsync<T, E> => {
|
||||||
|
return ResultAsync.fromPromise(operation(), (error) =>
|
||||||
|
dbErrorFactory(error)
|
||||||
|
).andThen((result) => (result === null ? err(notFoundError) : ok(result)));
|
||||||
|
};
|
||||||
2
packages/exceptions/src/utils/index.ts
Normal file
2
packages/exceptions/src/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './database';
|
||||||
|
export * from './validation';
|
||||||
10
packages/exceptions/src/utils/validation.ts
Normal file
10
packages/exceptions/src/utils/validation.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { err, ok, Result } from 'neverthrow';
|
||||||
|
import { output, ZodError, ZodObject } from 'zod';
|
||||||
|
|
||||||
|
export const safeParse =
|
||||||
|
<T extends ZodObject>(schema: T) =>
|
||||||
|
<D>(data: D): Result<output<T>, ZodError<output<T>>> => {
|
||||||
|
const parseResult = schema.safeParse(data);
|
||||||
|
|
||||||
|
return parseResult.success ? ok(parseResult.data) : err(parseResult.error);
|
||||||
|
};
|
||||||
20
packages/services/package.json
Normal file
20
packages/services/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@repo/services",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"types": "./src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@repo/db": "*",
|
||||||
|
"@repo/exceptions": "*",
|
||||||
|
"@repo/utils": "*",
|
||||||
|
"neverthrow": "^8.2.0",
|
||||||
|
"zod": "^4.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@repo/typescript-config": "*",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
packages/services/src/index.ts
Normal file
1
packages/services/src/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './payment';
|
||||||
20
packages/services/src/payment/errors.ts
Normal file
20
packages/services/src/payment/errors.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { AppError, createAppError } from '@repo/exceptions';
|
||||||
|
|
||||||
|
export enum PaymentErrorCode {
|
||||||
|
PAYMENT_NOT_FOUND = 'PAYMENT_NOT_FOUND',
|
||||||
|
PAYMENT_ALREADY_PROCESSED = 'PAYMENT_ALREADY_PROCESSED',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaymentError = AppError<PaymentErrorCode>;
|
||||||
|
|
||||||
|
export const paymentNotFound = (paymentId: string): PaymentError =>
|
||||||
|
createAppError<PaymentErrorCode>(
|
||||||
|
PaymentErrorCode.PAYMENT_NOT_FOUND,
|
||||||
|
`Payment with id ${paymentId} not found`
|
||||||
|
) as PaymentError;
|
||||||
|
|
||||||
|
export const paymentAlreadyProcessed = (paymentId: string): PaymentError =>
|
||||||
|
createAppError(
|
||||||
|
PaymentErrorCode.PAYMENT_ALREADY_PROCESSED,
|
||||||
|
`Payment ${paymentId} has already been processed`
|
||||||
|
) as PaymentError;
|
||||||
4
packages/services/src/payment/index.ts
Normal file
4
packages/services/src/payment/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './errors';
|
||||||
|
export * from './types';
|
||||||
|
export * from './repository';
|
||||||
|
export * from './service';
|
||||||
61
packages/services/src/payment/repository.ts
Normal file
61
packages/services/src/payment/repository.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { PaymentStatus, Prisma, PrismaClientArg, Payment } from '@repo/db';
|
||||||
|
import { paymentNotFound, PaymentError } from './errors';
|
||||||
|
import {
|
||||||
|
DatabaseError,
|
||||||
|
databaseError,
|
||||||
|
wrapDbOperation,
|
||||||
|
wrapDbOperationWithNull,
|
||||||
|
} from '@repo/exceptions';
|
||||||
|
import { ResultAsync } from 'neverthrow';
|
||||||
|
|
||||||
|
export const PaymentRepository = {
|
||||||
|
create(
|
||||||
|
client: PrismaClientArg,
|
||||||
|
data: Prisma.PaymentUncheckedCreateInput
|
||||||
|
): ResultAsync<Payment, DatabaseError> {
|
||||||
|
return wrapDbOperation(
|
||||||
|
() => client.payment.create({ data }).then((payment: Payment) => payment),
|
||||||
|
(e) => databaseError('create-error', e as Error)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
findById(
|
||||||
|
client: PrismaClientArg,
|
||||||
|
id: string
|
||||||
|
): ResultAsync<Payment, PaymentError | DatabaseError> {
|
||||||
|
return wrapDbOperationWithNull<Payment, PaymentError | DatabaseError>(
|
||||||
|
() => client.payment.findUnique({ where: { id } }),
|
||||||
|
paymentNotFound(id),
|
||||||
|
(e) => databaseError('find-by-id', e as Error)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStatus(
|
||||||
|
client: PrismaClientArg,
|
||||||
|
id: string,
|
||||||
|
status: PaymentStatus
|
||||||
|
): ResultAsync<Payment, DatabaseError> {
|
||||||
|
return wrapDbOperation(
|
||||||
|
() =>
|
||||||
|
client.payment.update({
|
||||||
|
where: { id },
|
||||||
|
data: { status },
|
||||||
|
}),
|
||||||
|
() => databaseError(id)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
findByUserId(
|
||||||
|
client: PrismaClientArg,
|
||||||
|
telegramUserId: string
|
||||||
|
): ResultAsync<Payment[], DatabaseError> {
|
||||||
|
return wrapDbOperation(
|
||||||
|
() =>
|
||||||
|
client.payment.findMany({
|
||||||
|
where: { telegramUserId },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
}),
|
||||||
|
() => databaseError(`user-${telegramUserId}`)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
35
packages/services/src/payment/service.ts
Normal file
35
packages/services/src/payment/service.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { ok, err } from 'neverthrow';
|
||||||
|
import { CreatePaymentInput, CreatePaymentInputSchema } from './types';
|
||||||
|
import { paymentAlreadyProcessed } from './errors';
|
||||||
|
import { PaymentRepository } from './repository';
|
||||||
|
import { Payment, PaymentStatus, PrismaClient } from '@repo/db';
|
||||||
|
import { safeParse } from '@repo/exceptions';
|
||||||
|
|
||||||
|
const validateInput = safeParse(CreatePaymentInputSchema);
|
||||||
|
|
||||||
|
export const createPayment = (
|
||||||
|
client: PrismaClient,
|
||||||
|
input: CreatePaymentInput
|
||||||
|
) =>
|
||||||
|
validateInput(input).map((validatedInput) =>
|
||||||
|
PaymentRepository.create(client, validatedInput)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const getPaymentById = (client: PrismaClient, id: string) =>
|
||||||
|
PaymentRepository.findById(client, id);
|
||||||
|
|
||||||
|
export const getUserPayments = (client: PrismaClient, telegramUserId: string) =>
|
||||||
|
PaymentRepository.findByUserId(client, telegramUserId);
|
||||||
|
|
||||||
|
export const validatePaymentForProcessing = (payment: Payment) =>
|
||||||
|
payment.status === PaymentStatus.PENDING
|
||||||
|
? ok(payment)
|
||||||
|
: err(paymentAlreadyProcessed(payment.id));
|
||||||
|
|
||||||
|
export const PaymentService = {
|
||||||
|
validateInput,
|
||||||
|
createPayment,
|
||||||
|
getPaymentById,
|
||||||
|
getUserPayments,
|
||||||
|
validatePaymentForProcessing,
|
||||||
|
};
|
||||||
50
packages/services/src/payment/types.ts
Normal file
50
packages/services/src/payment/types.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Payment, PaymentStatus, PrismaClient } from '@repo/db';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { ResultAsync } from 'neverthrow';
|
||||||
|
import { PaymentError } from './errors';
|
||||||
|
|
||||||
|
export enum Currency {
|
||||||
|
USD = 'USD',
|
||||||
|
EUR = 'EUR',
|
||||||
|
RUB = 'RUB',
|
||||||
|
XTR = 'XTR',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CURRENCIES = Object.values(Currency);
|
||||||
|
|
||||||
|
export enum PaymentProvider {
|
||||||
|
YOOKASSA = 'yookassa',
|
||||||
|
TELEGRAM = 'telegram',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PAYMENT_PROVIDERS = Object.values(PaymentProvider);
|
||||||
|
|
||||||
|
export const CreatePaymentInputSchema = z.object({
|
||||||
|
telegramUserId: z.uuid(),
|
||||||
|
amount: z.number().positive(),
|
||||||
|
currency: z.enum(Currency),
|
||||||
|
provider: z.enum(PaymentProvider),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CreatePaymentInput = z.infer<typeof CreatePaymentInputSchema>;
|
||||||
|
|
||||||
|
export type PaymentProviderResponse = {
|
||||||
|
readonly paymentUrl: string;
|
||||||
|
readonly providerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IPaymentProvider = {
|
||||||
|
readonly createPayment: (
|
||||||
|
client: PrismaClient,
|
||||||
|
input: CreatePaymentInput
|
||||||
|
) => ResultAsync<PaymentProviderResponse, PaymentError>;
|
||||||
|
readonly verifyPayment: (
|
||||||
|
client: PrismaClient,
|
||||||
|
payment: Payment
|
||||||
|
) => ResultAsync<PaymentStatus, PaymentError>;
|
||||||
|
readonly refundPayment: (
|
||||||
|
client: PrismaClient,
|
||||||
|
providerId: string,
|
||||||
|
amount?: number
|
||||||
|
) => ResultAsync<boolean, PaymentError>;
|
||||||
|
};
|
||||||
11
packages/services/tsconfig.json
Normal file
11
packages/services/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "@repo/typescript-config/base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
@@ -14,6 +14,11 @@
|
|||||||
"persistent": true,
|
"persistent": true,
|
||||||
"cache": false
|
"cache": false
|
||||||
},
|
},
|
||||||
|
"start": {
|
||||||
|
"dependsOn": ["^build"],
|
||||||
|
"persistent": true,
|
||||||
|
"cache": false
|
||||||
|
},
|
||||||
"db:generate": {
|
"db:generate": {
|
||||||
"cache": false
|
"cache": false
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user