diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..59a3820 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +Dockerfile +.dockerignore +.env +node_modules +npm-debug.log +dist \ No newline at end of file diff --git a/.env b/.env deleted file mode 100644 index 04b587b..0000000 --- a/.env +++ /dev/null @@ -1,7 +0,0 @@ -# Environment variables declared in this file are automatically made available to Prisma. -# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema - -# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. -# See the documentation for all the connection string options: https://pris.ly/d/connection-strings - -DATABASE_URL="postgresql://username:password@localhost:5434/mydb?schema=public" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 3c3629e..9c97bbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ node_modules +dist +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f77745c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ + +# Use the official Node.js 18 image as a base +FROM node:18.16.1-alpine + +# Install bash for debugging purposes (optional) +RUN apk add --no-cache bash + +# Install global Node.js packages for NestJS development +RUN npm i -g @nestjs/cli typescript ts-node + +# Set the working directory inside the container +WORKDIR /usr/src/app + +# Copy package.json and package-lock.json to the working directory +COPY package*.json ./ + +# Copy the Prisma configuration and migration files +# This line copies the "prisma" directory from your project's root into the Docker container's working directory. +COPY prisma ./prisma/ +COPY env-sample ./.env +# Install project dependencies +RUN npm install + +# Copy the rest of the application code to the container +COPY . . + +# Build your Nest.js application +RUN npm run build + +# Expose the PORT environment variable (default to 4022 if not provided)\\ +ENV PORT=4022 +EXPOSE $PORT + +# Start the Nest.js application using the start:prod script +CMD ["npm", "run", "start:prod"] diff --git a/README.md b/README.md index 00a13b1..a3ab31a 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,18 @@ -
+# Wallet Service -[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456 -[circleci-url]: https://circleci.com/gh/nestjs/nest +[Compass Product Flow](https://miro.com/app/board/uXjVMkv3bh4=/?share_link_id=179469421530) +[Compass Services Diagram](https://app.diagrams.net/#G1ZcWAg558z88DcWNC4b2NKt1Q3MAPHSZu) -A progressive Node.js framework for building efficient and scalable server-side applications.
- - +## Use Cases +Admin: + - Add/reduce credits to end user’s wallet + - View admin-user transaction history + - View admin-3CP transaction history -## Description +3CP: + - View transaction history with marketplace -[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository. - -## Installation - -```bash -$ npm install -``` - -## Running the app - -```bash -# development -$ npm run start - -# watch mode -$ npm run start:dev - -# production mode -$ npm run start:prod -``` - -## Test - -```bash -# unit tests -$ npm run test - -# e2e tests -$ npm run test:e2e - -# test coverage -$ npm run test:cov -``` - -## Support - -Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support). - -## Stay in touch - -- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com) -- Website - [https://nestjs.com](https://nestjs.com/) -- Twitter - [@nestframework](https://twitter.com/nestframework) - -## License - -Nest is [MIT licensed](LICENSE). +EndUser: + - View remaining credits + - View transaction history of credits + - Purchase courses with credits \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 1096b87..8d6ee52 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,15 @@ version: '3.8' services: - - postgres: - image: postgres:13.5 - restart: always - environment: - - POSTGRES_USER=username - - POSTGRES_PASSWORD=password - volumes: - - postgres:/var/lib/postgresql/data + wallet: + build: + context: . + dockerfile: Dockerfile + container_name: wallet-service ports: - - '5434:5432' + - 4022:4022 + networks: + - samagra_compass -volumes: - postgres: \ No newline at end of file +networks: + samagra_compass: + external: true diff --git a/env-sample b/env-sample new file mode 100644 index 0000000..9589809 --- /dev/null +++ b/env-sample @@ -0,0 +1,4 @@ +NODE_ENV=development +PORT= +DATABASE_URL= +TELEMETRY_DATABASE_NAME= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2b753dc..ca0e666 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,11 @@ "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", - "@prisma/client": "^5.3.1", + "@nestjs/swagger": "^7.1.12", + "@prisma/client": "^5.7.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "lodash": "^4.17.21", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1" }, @@ -22,6 +26,7 @@ "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/lodash": "^4.14.201", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.0.0", @@ -31,7 +36,7 @@ "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", "prettier": "^3.0.0", - "prisma": "^5.3.1", + "prisma": "^5.7.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "^29.1.0", @@ -1548,6 +1553,25 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz", + "integrity": "sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==", + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.5.tgz", @@ -1640,6 +1664,37 @@ "node": ">=12" } }, + "node_modules/@nestjs/swagger": { + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.12.tgz", + "integrity": "sha512-Q1P/IE+cws0sJeNtbs+8uDalcVylpmAnaEUFenGOa3KSNnXF/8DOE84mET/uUhFXsiz9PLHK8Hy7o7B6fRpMhg==", + "dependencies": { + "@nestjs/mapped-types": "2.0.2", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "3.2.0", + "swagger-ui-dist": "5.7.2" + }, + "peerDependencies": { + "@fastify/static": "^6.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.5.tgz", @@ -1740,13 +1795,10 @@ } }, "node_modules/@prisma/client": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.3.1.tgz", - "integrity": "sha512-ArOKjHwdFZIe1cGU56oIfy7wRuTn0FfZjGuU/AjgEBOQh+4rDkB6nF+AGHP8KaVpkBIiHGPQh3IpwQ3xDMdO0Q==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.7.0.tgz", + "integrity": "sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg==", "hasInstallScript": true, - "dependencies": { - "@prisma/engines-version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59" - }, "engines": { "node": ">=16.13" }, @@ -1759,17 +1811,56 @@ } } }, + "node_modules/@prisma/debug": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.0.tgz", + "integrity": "sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA==", + "devOptional": true + }, "node_modules/@prisma/engines": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.3.1.tgz", - "integrity": "sha512-6QkILNyfeeN67BNEPEtkgh3Xo2tm6D7V+UhrkBbRHqKw9CTaz/vvTP/ROwYSP/3JT2MtIutZm/EnhxUiuOPVDA==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.0.tgz", + "integrity": "sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA==", + "devOptional": true, + "hasInstallScript": true, + "dependencies": { + "@prisma/debug": "5.7.0", + "@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9", + "@prisma/fetch-engine": "5.7.0", + "@prisma/get-platform": "5.7.0" + } + }, + "node_modules/@prisma/engines/node_modules/@prisma/engines-version": { + "version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9.tgz", + "integrity": "sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==", + "devOptional": true + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.0.tgz", + "integrity": "sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w==", "devOptional": true, - "hasInstallScript": true + "dependencies": { + "@prisma/debug": "5.7.0", + "@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9", + "@prisma/get-platform": "5.7.0" + } + }, + "node_modules/@prisma/fetch-engine/node_modules/@prisma/engines-version": { + "version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9.tgz", + "integrity": "sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==", + "devOptional": true }, - "node_modules/@prisma/engines-version": { - "version": "5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.3.1-2.61e140623197a131c2a6189271ffee05a7aa9a59.tgz", - "integrity": "sha512-y5qbUi3ql2Xg7XraqcXEdMHh0MocBfnBzDn5GbV1xk23S3Mq8MGs+VjacTNiBh3dtEdUERCrUUG7Z3QaJ+h79w==" + "node_modules/@prisma/get-platform": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.0.tgz", + "integrity": "sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA==", + "devOptional": true, + "dependencies": { + "@prisma/debug": "5.7.0" + } }, "node_modules/@sinclair/typebox": { "version": "0.27.8", @@ -1990,6 +2081,12 @@ "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.201", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz", + "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -2072,6 +2169,11 @@ "@types/superagent": "*" } }, + "node_modules/@types/validator": { + "version": "13.11.1", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.1.tgz", + "integrity": "sha512-d/MUkJYdOeKycmm75Arql4M5+UuXmf4cHdHKsyw1GcvnNgL6s77UkgSgJ8TE/rI5PYsnwYq5jkcWBLuN/MpQ1A==" + }, "node_modules/@types/yargs": { "version": "17.0.24", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", @@ -2604,8 +2706,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-flatten": { "version": "1.1.1", @@ -3147,6 +3248,21 @@ "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==" + }, + "node_modules/class-validator": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.0.tgz", + "integrity": "sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==", + "dependencies": { + "@types/validator": "^13.7.10", + "libphonenumber-js": "^1.10.14", + "validator": "^13.7.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -5795,7 +5911,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "dependencies": { "argparse": "^2.0.1" }, @@ -5909,6 +6024,11 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.10.44", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.44.tgz", + "integrity": "sha512-svlRdNBI5WgBjRC20GrCfbFiclbF0Cx+sCcQob/C1r57nsoq0xg8r65QbTyVyweQIlB33P+Uahyho6EMYgcOyQ==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -5942,8 +6062,7 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -6747,13 +6866,13 @@ } }, "node_modules/prisma": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.3.1.tgz", - "integrity": "sha512-Wp2msQIlMPHe+5k5Od6xnsI/WNG7UJGgFUJgqv/ygc7kOECZapcSz/iU4NIEzISs3H1W9sFLjAPbg/gOqqtB7A==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.0.tgz", + "integrity": "sha512-0rcfXO2ErmGAtxnuTNHQT9ztL0zZheQjOI/VNJzdq87C3TlGPQtMqtM+KCwU6XtmkoEr7vbCQqA7HF9IY0ST+Q==", "devOptional": true, "hasInstallScript": true, "dependencies": { - "@prisma/engines": "5.3.1" + "@prisma/engines": "5.7.0" }, "bin": { "prisma": "build/index.js" @@ -7636,6 +7755,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.7.2.tgz", + "integrity": "sha512-mVZc9QVQ6pTCV5crli3+Ng+DoMPwdtMHK8QLk2oX8Mtamp4D/hV+uYdC3lV0JZrDgpNEcjs0RrWTqMwwosuLPQ==" + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -8193,6 +8317,14 @@ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", "dev": true }, + "node_modules/validator": { + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 8427f9b..2f29495 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "start": "nest start", "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", - "start:prod": "node dist/main", + "start:prod": "npx prisma migrate deploy && node dist/src/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", @@ -19,11 +19,18 @@ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:e2e": "jest --config ./test/jest-e2e.json" }, + "prisma": { + "seed": "ts-node prisma/seed.ts" + }, "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", - "@prisma/client": "^5.3.1", + "@nestjs/swagger": "^7.1.12", + "@prisma/client": "^5.7.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "lodash": "^4.17.21", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1" }, @@ -33,6 +40,7 @@ "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/lodash": "^4.14.201", "@types/node": "^20.3.1", "@types/supertest": "^2.0.12", "@typescript-eslint/eslint-plugin": "^6.0.0", @@ -42,7 +50,7 @@ "eslint-plugin-prettier": "^5.0.0", "jest": "^29.5.0", "prettier": "^3.0.0", - "prisma": "^5.3.1", + "prisma": "^5.7.0", "source-map-support": "^0.5.21", "supertest": "^6.3.3", "ts-jest": "^29.1.0", diff --git a/prisma/migrations/20230920065408_init/migration.sql b/prisma/migrations/20231214141343_first/migration.sql similarity index 67% rename from prisma/migrations/20230920065408_init/migration.sql rename to prisma/migrations/20231214141343_first/migration.sql index 9991203..cb46845 100644 --- a/prisma/migrations/20230920065408_init/migration.sql +++ b/prisma/migrations/20231214141343_first/migration.sql @@ -1,15 +1,18 @@ -- CreateEnum -CREATE TYPE "WalletType" AS ENUM ('admin', 'provider', 'user'); +CREATE TYPE "WalletType" AS ENUM ('ADMIN', 'PROVIDER', 'CONSUMER'); -- CreateEnum -CREATE TYPE "WalletStatus" AS ENUM ('active', 'inactive', 'frozen'); +CREATE TYPE "WalletStatus" AS ENUM ('ACTIVE', 'INACTIVE', 'FROZEN'); + +-- CreateEnum +CREATE TYPE "TransactionType" AS ENUM ('PURCHASE', 'ADD_CREDITS', 'REDUCE_CREDITS', 'SETTLEMENT', 'REFUND'); -- CreateTable CREATE TABLE "wallets" ( "walletId" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, + "userId" TEXT NOT NULL, "type" "WalletType" NOT NULL, - "status" "WalletStatus" NOT NULL, + "status" "WalletStatus" NOT NULL DEFAULT 'ACTIVE', "credits" INTEGER NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, @@ -23,12 +26,16 @@ CREATE TABLE "transactions" ( "fromId" INTEGER NOT NULL, "toId" INTEGER NOT NULL, "credits" INTEGER NOT NULL, - "description" TEXT NOT NULL, + "type" "TransactionType" NOT NULL, + "description" TEXT, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "transactions_pkey" PRIMARY KEY ("transactionId") ); +-- CreateIndex +CREATE UNIQUE INDEX "wallets_userId_key" ON "wallets"("userId"); + -- AddForeignKey ALTER TABLE "transactions" ADD CONSTRAINT "transactions_fromId_fkey" FOREIGN KEY ("fromId") REFERENCES "wallets"("walletId") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9b28819..2a3c353 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -11,22 +11,31 @@ datasource db { } enum WalletType { - admin - provider - user + ADMIN + PROVIDER + CONSUMER } enum WalletStatus { - active - inactive - frozen + ACTIVE + INACTIVE + FROZEN } +enum TransactionType { + PURCHASE + ADD_CREDITS + REDUCE_CREDITS + SETTLEMENT + REFUND +} + + model wallets { walletId Int @id @default(autoincrement()) - userId Int + userId String @unique type WalletType - status WalletStatus + status WalletStatus @default(ACTIVE) credits Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -39,7 +48,8 @@ model transactions { fromId Int toId Int credits Int - description String + type TransactionType + description String? createdAt DateTime @default(now()) from wallets @relation("FromTransaction", fields:[fromId], references:[walletId]) to wallets @relation("ToTransaction", fields:[toId], references:[walletId]) diff --git a/prisma/scripts/createViewQueries.ts b/prisma/scripts/createViewQueries.ts new file mode 100644 index 0000000..25e390e --- /dev/null +++ b/prisma/scripts/createViewQueries.ts @@ -0,0 +1,22 @@ +export const createViewQueries = [ + ` + DROP VIEW IF EXISTS telemetry_transactions + `, + + ` + CREATE VIEW telemetry_transactions AS + SELECT + wf."userId" AS "fromUserId", + wt."userId" AS "toUserId", + t.credits AS "amount", + t.type, + t.description, + t."createdAt" AS "transactionDate" + FROM + transactions t + JOIN + wallets wf ON t."fromId" = wf."walletId" + JOIN + wallets wt ON t."toId" = wt."walletId" + ` +]; \ No newline at end of file diff --git a/prisma/scripts/moveViewsQueries.ts b/prisma/scripts/moveViewsQueries.ts new file mode 100644 index 0000000..e3397a2 --- /dev/null +++ b/prisma/scripts/moveViewsQueries.ts @@ -0,0 +1,51 @@ +const dbName = process.env.DATABASE_NAME; +const dbUserName = process.env.DATABASE_USERNAME; +const dbPassword = process.env.DATABASE_PASSWORD; +const dbPort = process.env.DATABASE_PORT; +const dbHost = '172.17.0.1'; + +export const copyViewQueries = [ + ` + CREATE EXTENSION IF NOT EXISTS postgres_fdw + `, + ` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_foreign_server + WHERE srvname = 'wallet_server' + ) THEN + EXECUTE 'CREATE SERVER wallet_server + FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (host ''${dbHost}'', dbname ''${dbName}'', port ''${dbPort}'')'; + END IF; + END $$; + `, + ` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_user_mappings + WHERE srvname = 'wallet_server' AND usename = '${dbUserName}' + ) THEN + EXECUTE 'CREATE USER MAPPING FOR ${dbUserName} + SERVER wallet_server + OPTIONS (user ''${dbUserName}'', password ''${dbPassword}'')'; + END IF; + END $$; + `, + ` + CREATE FOREIGN TABLE telemetry_transactions ( + "fromUserId" text, + "toUserId" text, + "amount" integer, + "type" text, + "description" text, + "transactionDate" timestamp(3) without time zone + ) + SERVER wallet_server + OPTIONS (schema_name 'public', table_name 'telemetry_transactions'); + ` +]; \ No newline at end of file diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..5d70c42 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,195 @@ +import { Logger } from '@nestjs/common'; +import { Prisma, PrismaClient, TransactionType, WalletStatus, WalletType } from '@prisma/client' +import * as fs from "fs"; +import { createViewQueries } from './scripts/createViewQueries'; +import { copyViewQueries } from './scripts/moveViewsQueries'; +const { Client } = require('pg'); + +const prisma = new PrismaClient(); +const telemetryDbName = process.env.TELEMETRY_DATABASE_NAME; + +async function seed() { + + const wallet1 = await prisma.wallets.create({ + data: { + userId: "9f4611d4-ab92-4acd-b3ce-13594e362eca", + type: WalletType.CONSUMER, + credits: 125, + }, + }); + + const wallet2 = await prisma.wallets.create({ + data: { + userId: "7eddc220-f33d-476c-b204-041e584585c6", + type: WalletType.CONSUMER, + credits: 200 + }, + }); + + const wallet3 = await prisma.wallets.create({ + data: { + userId: "836ba369-fc24-4464-95ec-505d61b67ef0", + type: WalletType.CONSUMER, + credits: 100 + } + }); + + const wallet4 = await prisma.wallets.create({ + data: { + userId: "c8a43816-5a1b-4e29-9e1f-e8ef22efc669", + type: WalletType.CONSUMER, + credits: 300, + }, + }); + + const wallet5 = await prisma.wallets.create({ + data: { + userId: "123e4567-e89b-42d3-a456-556642440010", + type: WalletType.PROVIDER, + credits: 300, + }, + }); + + const wallet6 = await prisma.wallets.create({ + data: { + userId: "123e4567-e89b-42d3-a456-556642440011", + type: WalletType.PROVIDER, + credits: 300, + }, + }); + + const wallet7 = await prisma.wallets.create({ + data: { + userId: "123e4567-e89b-42d3-a456-556642440012", + type: WalletType.PROVIDER, + credits: 300, + }, + }); + + const wallet8 = await prisma.wallets.create({ + data: { + userId: "123e4567-e89b-42d3-a456-556642440013", + type: WalletType.PROVIDER, + credits: 300, + }, + }); + + const wallet9 = await prisma.wallets.create({ + data: { + userId: "890f2839-866f-4524-9eac-bebe0d35d607", + type: WalletType.ADMIN, + credits: 450, + }, + }); + + const wallet10 = await prisma.wallets.create({ + data: { + userId: "87fd80a9-63e9-4e90-81bb-4b6956c2561b", + type: WalletType.ADMIN, + credits: 300, + }, + }); + + const transaction1 = await prisma.transactions.create({ + data: { + credits: 100, + fromId: wallet9.walletId, + toId: wallet1.walletId, + type: TransactionType.ADD_CREDITS, + description: "Credits added by the admin" + } + }); + + const transaction2 = await prisma.transactions.create({ + data: { + credits: 20, + fromId: wallet1.walletId, + toId: wallet5.walletId, + type: TransactionType.PURCHASE, + description: "Purchased course ABC" + } + }); + + const transaction3 = await prisma.transactions.create({ + data: { + credits: 200, + fromId: wallet1.walletId, + toId: wallet5.walletId, + type: TransactionType.PURCHASE, + description: "Purchased course XYZ" + } + }); + + const transaction4 = await prisma.transactions.create({ + data: { + credits: 200, + fromId: wallet5.walletId, + toId: wallet9.walletId, + type: TransactionType.SETTLEMENT, + description: "Credit balance settled" + } + }) + console.log({ wallet1, wallet2, wallet3, wallet4, wallet5, wallet6, wallet7, wallet8, wallet9, wallet10, + transaction1, transaction2, transaction3, transaction4 }); +} + +async function createViews() { + let logger = new Logger("CreatingViews"); + logger.log(`Started creating views`); + + for (const sql of createViewQueries) { + logger.log(sql); + await prisma.$executeRaw`${Prisma.raw(sql)}`; + } + + const res:any = await prisma.$queryRaw`${Prisma.raw(`SELECT datname FROM pg_database WHERE datname = '${telemetryDbName}'`)}`; + if (res.length === 0) { + // Create the telemetry-views database if it does not exist + await prisma.$queryRaw`${Prisma.raw(`CREATE DATABASE "${telemetryDbName}"`)}`; + logger.log(`Database "${telemetryDbName}" created.`); + } else { + logger.log(`Database "${telemetryDbName}" already exists.`); + } + + logger.log(`Successfully created views`); +} + +async function moveViews() { + let logger = new Logger("MovingViews"); + + const telemetryClient = new Client({ + user: process.env.DATABASE_USERNAME, + host: '172.17.0.1', + database: process.env.TELEMETRY_DATABASE_NAME, + password: process.env.DATABASE_PASSWORD, + port: 5432, + }); + + await telemetryClient.connect(); + + logger.log(`Started moving views`); + + for (const sql of copyViewQueries) { + logger.log(sql); + await telemetryClient.query(sql); + } + + await telemetryClient.end(); + + logger.log(`Successfully moved views`); +} + +async function main() { + try { + await seed(); + await createViews(); + await moveViews(); + } catch (e) { + console.error(e); + process.exit(1); + } finally { + await prisma.$disconnect(); + } +} + +main(); \ No newline at end of file diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 7b5d784..3682c9b 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -1,4 +1,426 @@ -import { Controller } from '@nestjs/common'; +import { Body, Controller, Get, HttpStatus, Logger, Param, ParseUUIDPipe, Post, Res } from '@nestjs/common'; +import { AdminService } from './admin.service'; +import { TransactionService } from 'src/transactions/transactions.service'; +import { ConsumerService } from 'src/consumer/consumer.service'; +import { TransactionType } from '@prisma/client'; +import { ProviderService } from 'src/provider/provider.service'; +import { CreditsDto, ProviderCreditsDto, SettlementDto } from '../dto/credits.dto'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { Transaction } from 'src/transactions/dto/transactions.dto'; +import { WalletCredits } from 'src/wallet/dto/wallet.dto'; +import { getPrismaErrorStatusAndMessage } from 'src/utils/utils'; + +@ApiTags('admin') @Controller('admin') -export class AdminController {} +export class AdminController { + + private readonly logger = new Logger(AdminController.name); + + constructor( + private transactionService: TransactionService, + private consumerService: ConsumerService, + private adminService: AdminService, + private providerService: ProviderService + ) {} + + @ApiOperation({ summary: 'Get All Consumers Credits' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credits fetched successfully', type: [CreditsDto] }) + @Get("/:adminId/credits/consumers") + // get credits of all consumers + async getAllConsumersCredits( + @Param("adminId", ParseUUIDPipe) adminId: string, + @Res() res + ) { + try { + this.logger.log(`Validating admin`); + + // check admin + await this.adminService.getAdminWallet(adminId); + + this.logger.log(`Getting all consumer credits`); + + // fetch credits + const creditsResponse = await this.consumerService.getAllConsumersCredits(); + + this.logger.log(`Successfully retrieved credits`); + + return res.status(HttpStatus.OK).json({ + message: "credits fetched successfully", + data: { + credits: creditsResponse + } + }); + } catch (err) { + this.logger.error(`Failed to retreive credits`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch credits", + }); + } + } + + @ApiOperation({ summary: 'Get All Consumers Transactions' }) + @ApiResponse({ status: HttpStatus.OK, description: 'transactions fetched successfully', type: [Transaction] }) + @Get("/:adminId/transactions/consumers") + // get all transactions of all consumers + async getAllConsumersTransactions( + @Param("adminId", ParseUUIDPipe) adminId: string, + @Res() res + ) { + try { + this.logger.log(`Validating admin`); + + // check admin + await this.adminService.getAdminWallet(adminId); + + this.logger.log(`Getting all consumer transactions`); + + // fetch transactions + const transactions = await this.transactionService.fetchAllConsumersTransactions(); + + this.logger.log(`Successfully retrieved all the transactions`); + + return res.status(HttpStatus.OK).json({ + message: "transactions fetched successfully", + data: { + transactions + } + }); + } catch (err) { + this.logger.error(`Failed to retreive all the transactions`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch all the transactions", + }); + } + } + + @ApiOperation({ summary: 'Get One Consumer Transactions' }) + @ApiResponse({ status: HttpStatus.OK, description: 'transactions fetched successfully', type: [Transaction] }) + @Get("/:adminId/transactions/consumers/:consumerId") + // get all transactions of a particular consumer + async getConsumerTransactions( + @Param("adminId", ParseUUIDPipe) adminId: string, + @Param("consumerId", ParseUUIDPipe) consumerId: string, + @Res() res + ) { + try { + this.logger.log(`Validating admin`); + + // check admin + await this.adminService.getAdminWallet(adminId); + + this.logger.log(`Validating Consumer`); + + // check consumer + await this.consumerService.getConsumerWallet(consumerId); + + this.logger.log(`Getting the consumer transactions`); + + // fetch transactions + const transactions = await this.transactionService.fetchTransactionsOfOneUser(consumerId); + + this.logger.log(`Successfully retrieved all the transactions`); + + return res.status(HttpStatus.OK).json({ + message: "transactions fetched successfully", + data: { + transactions + } + }); + } catch (err) { + this.logger.error(`Failed to retreive the transactions: `, err.message); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the transactions", + }); + } + } + + @ApiOperation({ summary: 'Get All Providers Credits' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credits fetched successfully', type: [ProviderCreditsDto] }) + @Get("/:adminId/credits/providers") + // get credits of all providers + async getAllProvidersCredits( + @Param("adminId", ParseUUIDPipe) adminId: string, + @Res() res + ) { + try { + this.logger.log(`Validating admin`); + + // check admin + await this.adminService.getAdminWallet(adminId); + + this.logger.log(`Getting all provider credits`); + + // fetch credits + const creditsResponse = await this.providerService.getAllProvidersCredits(); + + this.logger.log(`Successfully retrieved credits`); + + return res.status(HttpStatus.OK).json({ + message: "credits fetched successfully", + data: { + credits: creditsResponse + } + }); + } catch (err) { + this.logger.error(`Failed to retreive credits: `,err.message); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch credits", + }); + } + } + + @ApiOperation({ summary: 'Get All Admin Providers Transactions' }) + @ApiResponse({ status: HttpStatus.OK, description: 'transactions fetched successfully', type: [Transaction] }) + @Get("/:adminId/transactions/providers") + // get all transactions between all providers and admins + async getAllAdminProvidersTransactions( + @Param("adminId", ParseUUIDPipe) adminId: string, + @Res() res + ) { + try { + this.logger.log(`Validating admin`); + + // check admin + await this.adminService.getAdminWallet(adminId); + + this.logger.log(`Getting all providers transactions`); + + // fetch transactions + const transactions = await this.transactionService.fetchAllAdminProviderTransactions(); + + this.logger.log(`Successfully retrieved all the transactions`); + + return res.status(HttpStatus.OK).json({ + message: "transactions fetched successfully", + data: { + transactions + } + }); + } catch (err) { + this.logger.error(`Failed to retreive the transactions: `,err.message); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the transactions", + }); + } + } + + @ApiOperation({ summary: 'Get One Provider Transactions' }) + @ApiResponse({ status: HttpStatus.OK, description: 'transactions fetched successfully', type: [Transaction] }) + @Get("/:adminId/transactions/providers/:providerId") + // get all transactions of a particular provider + async getProviderTransactions( + @Param("adminId", ParseUUIDPipe) adminId: string, + @Param("providerId", ParseUUIDPipe) providerId: string, + @Res() res + ) { + try { + this.logger.log(`Validating admin`); + + // check admin + await this.adminService.getAdminWallet(adminId); + + this.logger.log(`Validating Provider`); + + // check provider + await this.providerService.getProviderWallet(providerId); + + this.logger.log(`Getting the providers transactions`); + + // fetch transactions + const transactions = await this.transactionService.fetchTransactionsOfOneUser(providerId); + + this.logger.log(`Successfully retrieved all the transactions`); + + return res.status(HttpStatus.OK).json({ + message: "transactions fetched successfully", + data: { + transactions + } + }); + } catch (err) { + this.logger.error(`Failed to retreive the transactions: `,err.message); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the transactions", + }); + } + } + + @ApiOperation({ summary: 'Add Credits' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credits added successfully', type: WalletCredits }) + @Post("/:adminId/add-credits") + // add credits to a consumer's wallet + async addCredits( + @Param("adminId", ParseUUIDPipe) adminId: string, + @Body() creditsDto: CreditsDto, + @Res() res + ) { + try { + this.logger.log(`Validating admin`); + + // check admin + const adminWallet = await this.adminService.getAdminWallet(adminId); + + this.logger.log(`Updating Consumer wallet`); + + // update wallet + const consumerWallet = await this.consumerService.addCreditsToConsumer(creditsDto.consumerId, creditsDto.credits); + + this.logger.log(`Creating transaction`); + + // create transaction + await this.transactionService.createTransaction( + creditsDto.credits, + adminWallet.walletId, + consumerWallet.walletId, + TransactionType.ADD_CREDITS, + "Credits added by the admin" + ); + + this.logger.log(`Successfully added credits`); + + return res.status(HttpStatus.OK).json({ + message: "Credits added successfully", + data: { + credits: consumerWallet.credits + } + }); + } catch (err) { + this.logger.error(`Failed to add credits: `,err.message); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to add credits", + }); + } + } + + @ApiOperation({ summary: 'Reduce Credits' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Credits added successfully', type: WalletCredits }) + @Post("/:adminId/reduce-credits") + // reduce credits from a consumer's wallet + async reduceCredits( + @Param("adminId", ParseUUIDPipe) adminId: string, + @Body() creditsDto: CreditsDto, + @Res() res + ) { + try { + this.logger.log(`Validating admin`); + + // check admin + const adminWallet = await this.adminService.getAdminWallet(adminId); + + this.logger.log(`Updating Consumer wallet`); + + // update wallet + const consumerWallet = await this.consumerService.reduceConsumerCredits(creditsDto.consumerId, creditsDto.credits); + + this.logger.log(`Creating transaction`); + + // create transaction + await this.transactionService.createTransaction( + creditsDto.credits, + consumerWallet.walletId, + adminWallet.walletId, + TransactionType.REDUCE_CREDITS, + "Credits reduced by the admin" + ); + + this.logger.log(`Successfully reduced credits`); + + return res.status(HttpStatus.OK).json({ + message: "Credits reduced successfully", + data: { + credits: consumerWallet.credits + } + }) + } catch (err) { + this.logger.error(`Failed to reduce credits: `,err.message); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to reduce credits", + }); + } + } + + @ApiOperation({ summary: 'Transfer settlement credits and record transaction' }) + @ApiResponse({ status: HttpStatus.OK, description: 'credits transferred successfully', type: Transaction }) + @Post("/:adminId/providers/:providerId/settle-credits") + // Transfer credits from provider wallet to admin wallet + async settleProviderWallet( + @Param("adminId", ParseUUIDPipe) adminId: string, + @Param("providerId", ParseUUIDPipe) providerId: string, + @Res() res + ) { + try { + this.logger.log(`Validating admin`); + + // check admin + const adminWallet = await this.adminService.getAdminWallet(adminId); + + this.logger.log(`Getting provider wallet`); + + // fetch wallet + const providerWallet = await this.providerService.getProviderWallet(providerId); + + this.logger.log(`Updating admin wallet`); + + // update admin wallet + await this.adminService.addCreditsToAdmin(adminId, providerWallet.credits); + + this.logger.log(`Updating provider wallet`); + + // update provider wallet + await this.providerService.reduceProviderCredits(providerId, providerWallet.credits); + + this.logger.log(`Creating transaction`); + + // create transaction + const transaction = + await this.transactionService.createTransaction( + providerWallet.credits, + providerWallet.walletId, + adminWallet.walletId, + TransactionType.SETTLEMENT, + ); + + this.logger.log(`Successfully settled credits`); + + return res.status(HttpStatus.OK).json({ + message: "credits transferred successfully", + data: { + transaction + } + }) + } catch (err) { + this.logger.error(`Failed to settle the credits: `,err.message); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to settle the credits", + }); + } + } +} diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts index 4b37e20..beb704d 100644 --- a/src/admin/admin.module.ts +++ b/src/admin/admin.module.ts @@ -1,9 +1,15 @@ import { Module } from '@nestjs/common'; import { AdminController } from './admin.controller'; import { AdminService } from './admin.service'; +import { TransactionService } from 'src/transactions/transactions.service'; +import { WalletService } from 'src/wallet/wallet.service'; +import { ConsumerService } from 'src/consumer/consumer.service'; +import { ProviderService } from 'src/provider/provider.service'; +import { PrismaModule } from 'src/prisma/prisma.module'; @Module({ + imports: [PrismaModule], controllers: [AdminController], - providers: [AdminService] + providers: [AdminService, TransactionService, WalletService, ConsumerService, ProviderService] }) export class AdminModule {} diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 796f9fd..22b140c 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -1,4 +1,34 @@ -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { WalletType } from '@prisma/client'; +import { WalletService } from 'src/wallet/wallet.service'; @Injectable() -export class AdminService {} +export class AdminService { + constructor( + private walletService: WalletService, + ) {} + + async getAdminWallet(adminId: string) { + // get admin wallet + const adminWallet = await this.walletService.fetchWallet(adminId); + if(adminWallet == null) { + throw new NotFoundException("Admin Wallet does not exist"); + } + + // check admin + if(adminWallet.type != WalletType.ADMIN) { + throw new BadRequestException("Wallet does not belong to admin"); + } + return adminWallet; + } + + async addCreditsToAdmin(adminId: string, credits: number) { + + // check admin + let adminWallet = await this.getAdminWallet(adminId) + + // update provider wallet + adminWallet = await this.walletService.updateWalletCredits(adminId, adminWallet.credits + credits); + return adminWallet; + } +} diff --git a/src/app.controller.ts b/src/app.controller.ts index cce879e..cebbbbb 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -4,9 +4,5 @@ import { AppService } from './app.service'; @Controller() export class AppController { constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } + } diff --git a/src/app.module.ts b/src/app.module.ts index f5f837c..c9fb139 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,11 +3,12 @@ import { AppController } from './app.controller'; import { AppService } from './app.service'; import { AdminModule } from './admin/admin.module'; import { ProviderModule } from './provider/provider.module'; -import { UserModule } from './user/user.module'; +import { ConsumerModule } from './consumer/consumer.module'; import { PrismaModule } from './prisma/prisma.module'; +import { WalletModule } from './wallet/wallet.module'; @Module({ - imports: [AdminModule, ProviderModule, UserModule, PrismaModule], + imports: [AdminModule, ProviderModule, ConsumerModule, PrismaModule, WalletModule], controllers: [AppController], providers: [AppService], }) diff --git a/src/user/user.controller.spec.ts b/src/consumer/consumer.controller.spec.ts similarity index 51% rename from src/user/user.controller.spec.ts rename to src/consumer/consumer.controller.spec.ts index 7057a1a..be9814d 100644 --- a/src/user/user.controller.spec.ts +++ b/src/consumer/consumer.controller.spec.ts @@ -1,15 +1,15 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { UserController } from './user.controller'; +import { ConsumerController } from './consumer.controller'; -describe('UserController', () => { - let controller: UserController; +describe('ConsumerController', () => { + let controller: ConsumerController; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - controllers: [UserController], + controllers: [ConsumerController], }).compile(); - controller = module.get