From 17ce1bae65ef076f6dac3ce18e66264e271c69c6 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Thu, 19 Oct 2023 19:29:21 +0530 Subject: [PATCH 01/88] DB schema --- prisma/schema.prisma | 82 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e1c9ebf..a6c4b40 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,9 +12,81 @@ datasource db { // Here goes models -//dummy user model -model User { - id Int @id @default(autoincrement()) - email String @unique - name String? +enum ProviderStatus { + pending + verified + rejected +} + +enum CourseVerificationStatus { + pending + accepted + rejected +} + +enum CourseStatus { + active + inactive + archived +} + +enum CourseProgressStatus { + inProgress + completed +} + + +model Admin { + id Int @id @default(autoincrement()) + name String + email String @unique + password String + walletId Int +} + +model Provider { + id Int @id @default(autoincrement()) + name String + email String @unique + password String + walletId Int + paymentInfo Json? + status ProviderStatus @default(pending) + courses Course[] +} + +model Course { + id Int @id @default(autoincrement()) + providerId Int + title String + description String + courseLink String + imgLink String + credits Int + noOfLessons Int + language String + duration Int + competency Json[] + competencyIDs Int[] + author String + avgRating Float? + status CourseStatus + availabilityTime DateTime? + verificationStatus CourseVerificationStatus @default(pending) + cqfScore Int? + impactScore Float? + provider Provider @relation(fields: [providerId], references: [id]) + userCourses UserCourse[] +} + +model UserCourse { + id Int @id @default(autoincrement()) + userId String + courseId Int + purchasedAt DateTime + status CourseProgressStatus + courseCompletionScore Float? + rating Int? + feedback String? + course Course @relation(fields: [courseId], references: [id]) } From 95e2c95899b24cb5147eb241e8d73cf50ae0ce10 Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Wed, 25 Oct 2023 13:07:26 +0530 Subject: [PATCH 02/88] Added Admin APIs --- package-lock.json | 55 ++++++++++++++++++++++++++++--- package.json | 3 +- prisma/schema.prisma | 11 +++++-- src/app.module.ts | 2 ++ src/prisma/prisma.module.ts | 6 +++- src/prisma/prisma.service.spec.ts | 3 +- src/prisma/prisma.service.ts | 13 +++++++- src/utils/utils.ts | 30 +++++++++++++++++ 8 files changed, 111 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index d51118f..254c7d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@nestjs/axios": "^3.0.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^9.0.0", @@ -1396,6 +1397,17 @@ "node": ">=8" } }, + "node_modules/@nestjs/axios": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.0.tgz", + "integrity": "sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g==", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "reflect-metadata": "^0.1.12", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.5.0.tgz", @@ -2816,8 +2828,18 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", + "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } }, "node_modules/babel-jest": { "version": "28.1.3", @@ -3379,7 +3401,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3576,7 +3597,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -4389,6 +4409,26 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "peer": true, + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", @@ -4421,7 +4461,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -6755,6 +6794,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "peer": true + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", diff --git a/package.json b/package.json index 59374d7..ae07aee 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "prisma:seed": "npx prisma db seed" }, "dependencies": { + "@nestjs/axios": "^3.0.0", "@nestjs/common": "^9.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^9.0.0", @@ -83,4 +84,4 @@ "prisma": { "seed": "ts-node prisma/seed.ts" } -} \ No newline at end of file +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a6c4b40..f1c0438 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -35,6 +35,12 @@ enum CourseProgressStatus { completed } +enum TransactionType { + purchase + creditRequest + settlement +} + model Admin { id Int @id @default(autoincrement()) @@ -64,10 +70,9 @@ model Course { imgLink String credits Int noOfLessons Int - language String + language String[] duration Int - competency Json[] - competencyIDs Int[] + competency Json author String avgRating Float? status CourseStatus diff --git a/src/app.module.ts b/src/app.module.ts index 9046544..d3ac4c5 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,6 +4,7 @@ import { AppController } from "./app.controller"; import { AppService } from "./app.service"; import { PrismaModule } from "./prisma/prisma.module"; import { PrismaService } from "./prisma/prisma.service"; +import { AdminModule } from './admin/admin.module'; @Module({ imports: [ @@ -11,6 +12,7 @@ import { PrismaService } from "./prisma/prisma.service"; isGlobal: true, }), PrismaModule, + AdminModule, ], controllers: [AppController], providers: [AppService, PrismaService], diff --git a/src/prisma/prisma.module.ts b/src/prisma/prisma.module.ts index e6d1e9e..efd8d91 100644 --- a/src/prisma/prisma.module.ts +++ b/src/prisma/prisma.module.ts @@ -1,4 +1,8 @@ import { Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; +import { ConfigService } from '@nestjs/config'; -@Module({}) +@Module({ + providers: [PrismaService, ConfigService] +}) export class PrismaModule {} diff --git a/src/prisma/prisma.service.spec.ts b/src/prisma/prisma.service.spec.ts index a68cb9e..f1b91f3 100644 --- a/src/prisma/prisma.service.spec.ts +++ b/src/prisma/prisma.service.spec.ts @@ -1,12 +1,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { PrismaService } from './prisma.service'; +import { ConfigService } from '@nestjs/config'; describe('PrismaService', () => { let service: PrismaService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - providers: [PrismaService], + providers: [PrismaService, ConfigService], }).compile(); service = module.get(PrismaService); diff --git a/src/prisma/prisma.service.ts b/src/prisma/prisma.service.ts index 5a993ff..fe30953 100644 --- a/src/prisma/prisma.service.ts +++ b/src/prisma/prisma.service.ts @@ -1,5 +1,16 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { PrismaClient } from '@prisma/client'; @Injectable() -export class PrismaService extends PrismaClient{} +export class PrismaService extends PrismaClient{ + constructor(private config: ConfigService) { + super({ + datasources: { + db: { + url: config.get("DATABASE_URL"), + }, + }, + }); + } +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 7ecc693..accd650 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,3 +1,6 @@ +import { HttpStatus } from "@nestjs/common"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; + export const validationOptions = { whitelist: true, transform: true, @@ -6,3 +9,30 @@ export const validationOptions = { enableImplicitConversion: true, }, }; + +export function getPrismaErrorStatusAndMessage(error: any): { + errorMessage: string | undefined; + statusCode: number; +} { + if(error instanceof PrismaClientKnownRequestError) { + const errorCode = error?.code || "DEFAULT_ERROR_CODE"; + const errorCodeMap: Record = { + P2000: HttpStatus.BAD_REQUEST, + P2002: HttpStatus.CONFLICT, + P2003: HttpStatus.CONFLICT, + P2025: HttpStatus.NOT_FOUND, + }; + + const statusCode = errorCodeMap[errorCode] || HttpStatus.INTERNAL_SERVER_ERROR; + const errorMessage = error.message.split('\n').pop(); + return { statusCode, errorMessage }; + } + + const statusCode = + error?.status || error?.response?.status || HttpStatus.INTERNAL_SERVER_ERROR; + + return { + statusCode, + errorMessage: error.message, + }; +} From 4aab8110d1295d8b72e1e51acb3fa7c06458c3c1 Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Wed, 25 Oct 2023 13:14:04 +0530 Subject: [PATCH 03/88] Added admin APIs --- src/admin/admin.controller.spec.ts | 24 ++ src/admin/admin.controller.ts | 279 +++++++++++++++++++ src/admin/admin.module.ts | 12 + src/admin/admin.service.spec.ts | 20 ++ src/admin/admin.service.ts | 156 +++++++++++ src/admin/dto/CourseResponse.dto.ts | 22 ++ src/admin/dto/EditProvider.dto.ts | 10 + src/admin/dto/ProviderProfileResponse.dto.ts | 11 + src/admin/dto/TransactionResponse.dto.ts | 18 ++ src/admin/dto/VerifyCourse.dto.ts | 6 + 10 files changed, 558 insertions(+) create mode 100644 src/admin/admin.controller.spec.ts create mode 100644 src/admin/admin.controller.ts create mode 100644 src/admin/admin.module.ts create mode 100644 src/admin/admin.service.spec.ts create mode 100644 src/admin/admin.service.ts create mode 100644 src/admin/dto/CourseResponse.dto.ts create mode 100644 src/admin/dto/EditProvider.dto.ts create mode 100644 src/admin/dto/ProviderProfileResponse.dto.ts create mode 100644 src/admin/dto/TransactionResponse.dto.ts create mode 100644 src/admin/dto/VerifyCourse.dto.ts diff --git a/src/admin/admin.controller.spec.ts b/src/admin/admin.controller.spec.ts new file mode 100644 index 0000000..c854e7e --- /dev/null +++ b/src/admin/admin.controller.spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminController } from './admin.controller'; +import { AdminService } from './admin.service'; +import { PrismaModule } from 'nestjs-prisma'; +import { PrismaService } from '../prisma/prisma.service'; +import { ConfigService } from '@nestjs/config'; + +describe('AdminController', () => { + let controller: AdminController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [PrismaModule], + controllers: [AdminController], + providers: [AdminService, PrismaService, ConfigService] + }).compile(); + + controller = module.get(AdminController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts new file mode 100644 index 0000000..2bd4e8f --- /dev/null +++ b/src/admin/admin.controller.ts @@ -0,0 +1,279 @@ +import { Controller, Body, Get, Post, Patch, Res, Delete, HttpStatus, Param, ParseIntPipe, Logger} from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { AdminService } from './admin.service'; +import { ProviderProfileResponse } from './dto/ProviderProfileResponse.dto'; +import { getPrismaErrorStatusAndMessage } from '../utils/utils'; +import { EditProvider } from './dto/EditProvider.dto'; +import { Course } from '@prisma/client'; +import { CourseResponse } from './dto/CourseResponse.dto'; +import { CourseVerify } from './dto/VerifyCourse.dto'; +import { TransactionResponse } from './dto/TransactionResponse.dto'; + +@Controller('/api/admin') +@ApiTags('admin') +export class AdminController { + private readonly logger = new Logger(AdminController.name); + + constructor(private adminService: AdminService) {} + + @ApiOperation({ summary: "Get all providers" }) + @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse, isArray: true}) + @Get('/providers') + async getAllProviders(@Res() res): Promise { + try { + this.logger.log(`Getting information of all the providers`); + + const providers = await this.adminService.findAllProviders(); + + this.logger.log(`Successfully retrieved all the providers`); + + return res.status(HttpStatus.OK).json({ + message: "All providers fetched", + data: providers + }); + } catch (err) { + this.logger.error(`Failed to retreive all the providers' information`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + return res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch all the providers' information", + }); + } + } + + @ApiOperation({ summary: "View provider profile information"}) + @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse}) + @Get('/providers/:providerId') + async getProviderProfile ( + @Param("providerId", ParseIntPipe) providerId: number, @Res() res + ) : Promise{ + try { + this.logger.log(`Getting provider information for id ${providerId}`); + + const provider = await this.adminService.findProviderById(providerId); + + this.logger.log(`Successfully retrieved the provider profile information`); + + return res.status(HttpStatus.OK).json({ + message: "Provider profile retrieved successfully", + data: provider + }); + } catch (err) { + this.logger.error(`Failed to retrieve the provider profile information`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + return res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to retrieve the profile information for the given providerId", + }); + } + } + + + @ApiOperation({ summary: "Edit provider profile information"}) + @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse}) + @Patch('/providers/:providerId') + async editProviderProfile ( + @Param("providerId", ParseIntPipe) providerId: number, @Body() providerDto: EditProvider ,@Res() res + ) : Promise{ + try { + this.logger.log(`Getting provider information for id ${providerId}`); + + const updatedProviderInfo = { + id: providerDto.id, + name: providerDto.name, + email: providerDto.email, + paymentInfo: providerDto.paymentInfo, + walletId: providerDto.walletId, + status: providerDto.status + } + + const updatedProfile = await this.adminService.editProviderProfile(updatedProviderInfo); + + this.logger.log(`Successfully retrieved the provider profile information`); + + return res.status(HttpStatus.OK).json({ + message: "Provider profile retrieved successfully", + data: updatedProfile + }); + } catch (err) { + this.logger.error(`Failed to retrieve the provider profile information`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + return res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to retrieve the profile information for the given providerId", + }); + } + } + + @ApiOperation({ summary: "Get all the courses"}) + @ApiResponse({ status: HttpStatus.OK, type: CourseResponse, isArray: true}) + @Patch('/courses/') + async getAllCoursess(@Res() res) : Promise{ + try { + this.logger.log(`Fetching all courses on marketplace (verified, pending & rejected)`); + + const courses = await this.adminService.findAllCourses(); + + this.logger.log(`Successfully retrieved all the courses`); + + return res.status(HttpStatus.OK).json({ + message: "All courses retrieved successfully", + data: courses + }); + } catch (err) { + this.logger.error(`Failed to retrieve all courses`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + return res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to retrieve all courses", + }); + } + } + + @ApiOperation({ summary: "Get a course, given its courseId"}) + @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) + @Get('/course/:courseId') + async getCourseById ( + @Param("courseId", ParseIntPipe) courseId: number, @Res() res + ) : Promise{ + try { + this.logger.log(`Getting course information for id ${courseId}`); + + const course = this.adminService.findCourseById(courseId); + + this.logger.log(`Successfully retrieved the course`); + + return res.status(HttpStatus.OK).json({ + message: "Course retrieved successfully", + data: course + }); + } catch (err) { + this.logger.error(`Failed to retrieve the course for the courseId ${courseId}`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to retrieve the course with id ${courseId}`, + }); + } + } + + + @ApiOperation({ summary: "Accept course and assign a cqf_score"}) + @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) + @Patch('/course/:courseId/accept') + async acceptCourse ( + @Param("courseId", ParseIntPipe) courseId: number, @Body() verifyBody: CourseVerify, @Res() res + ) : Promise{ + try { + this.logger.log(`Verifying the course with id ${courseId}`); + + const course = this.adminService.acceptCourse(courseId, verifyBody.cqf_score); + + this.logger.log(`Successfully accepted the course`); + + return res.status(HttpStatus.OK).json({ + message: "Course accepted successfully", + data: course + }); + } catch (err) { + this.logger.error(`Failed to accept the course for the courseId ${courseId}`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to accept the course with id ${courseId}`, + }); + } + } + + @ApiOperation({ summary: "Reject a course given its courseId"}) + @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) + @Patch('/course/:courseId/reject') + async rejectCourse ( + @Param("courseId", ParseIntPipe) courseId: number, @Res() res + ) : Promise{ + try { + this.logger.log(`Processing reject request of course with id ${courseId}`); + + const course = this.adminService.rejectCourse(courseId); + + this.logger.log(`Successfully rejected the course`); + + return res.status(HttpStatus.OK).json({ + message: "Course rejected successfully", + data: course + }); + } catch (err) { + this.logger.error(`Failed to reject the course with the courseId ${courseId}`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to reject the course with id ${courseId}`, + }); + } + } + + @ApiOperation({ summary: "Remove a course given its courseId"}) + @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) + @Delete('/course/:courseId') + async removeCourse ( + @Param("courseId", ParseIntPipe) courseId: number, @Res() res + ) : Promise{ + try { + this.logger.log(`Processing removal request of course with id ${courseId}`); + + const course = this.adminService.removeCourse(courseId); + + this.logger.log(`Successfully deleted the course`); + + return res.status(HttpStatus.OK).json({ + message: "Course deleted successfully", + data: course + }); + } catch (err) { + this.logger.error(`Failed to delete the course with the courseId ${courseId}`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to delete the course with id ${courseId}`, + }); + } + } + + + + @ApiOperation({ summary: "Get transaction history between admin and consumers"}) + @ApiResponse({ status: HttpStatus.OK, type: TransactionResponse, isArray: true}) + @Get('/:adminId/transactions/consumers') + async getTransactions (@Param("adminId") adminId: number, @Res() res + ) : Promise{ + try { + this.logger.log(`Getting all transactions between admin and consumers.`); + + const transactions = this.adminService.getTransactions(adminId); + + this.logger.log(`Successfully fetched all the transactions between admin and consumers`); + + return res.status(HttpStatus.OK).json({ + message: "Fetched admin-consumers transactions", + data: transactions + }); + } catch (err) { + this.logger.error(`Failed to fetch the transactions between admin and consumers`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + return res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to fetch the transactions between admin and consumers`, + }); + } + } + +} diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts new file mode 100644 index 0000000..1663260 --- /dev/null +++ b/src/admin/admin.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { AdminController } from './admin.controller'; +import { AdminService } from './admin.service'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { PrismaModule } from 'nestjs-prisma'; + +@Module({ + imports: [PrismaModule], + controllers: [AdminController], + providers: [AdminService, PrismaService] +}) +export class AdminModule {} diff --git a/src/admin/admin.service.spec.ts b/src/admin/admin.service.spec.ts new file mode 100644 index 0000000..b3f1aae --- /dev/null +++ b/src/admin/admin.service.spec.ts @@ -0,0 +1,20 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminService } from './admin.service'; +import { PrismaService } from '../prisma/prisma.service'; +import { ConfigService } from '@nestjs/config'; + +describe('AdminService', () => { + let service: AdminService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AdminService, PrismaService, ConfigService], + }).compile(); + + service = module.get(AdminService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts new file mode 100644 index 0000000..2e52dd8 --- /dev/null +++ b/src/admin/admin.service.ts @@ -0,0 +1,156 @@ + +import { HttpException, Injectable, NotFoundException } from '@nestjs/common'; +import axios, { AxiosResponse } from 'axios'; +import { PrismaService } from '../prisma/prisma.service'; +import { Provider, ProviderStatus, Course, CourseVerificationStatus } from '@prisma/client'; + +@Injectable() +export class AdminService { + + constructor(private prisma: PrismaService) {} + + async verifyProvider(providerId: number) { + let providerInfo = await this.prisma.provider.findUnique({ + where: { id: providerId } + }); + + if(!providerInfo) { + throw new NotFoundException(`Provider with id ${providerId} does not exist`); + } + + if(providerInfo.status != ProviderStatus.pending) { + throw new HttpException(`Provider is either verified or rejected.`, 406); + } + + const updatedRecord = await this.prisma.provider.update({ + where: {id: providerId}, + data: {status: ProviderStatus.verified} + }); + return updatedRecord; + } + + async findAllProviders(): Promise { + let providers = await this.prisma.provider.findMany(); + return providers; + } + + async findProviderById(providerId: number): Promise { + let provider = await this.prisma.provider.findFirst({ + where: {id: providerId} + }); + + if(!provider) { + throw new NotFoundException(`Provider with id ${providerId} does not exist`); + } + + return provider; + } + + async findAllCourses() : Promise { + let courses = this.prisma.course.findMany(); + return courses; + } + + async findCourseById(courseId: number) { + let course = await this.prisma.course.findUnique({ + where: { id: courseId } + }); + if(!course) { + throw new NotFoundException(`Course with id ${courseId} not found`); + } + return course; + } + + async acceptCourse(courseId: number, cqf_score: number) { + let course = await this.prisma.course.findUnique({ + where: { id: courseId } + }); + if(!course) { + throw new NotFoundException(`Course with id ${courseId} not found`); + } + if(course.verificationStatus != CourseVerificationStatus.pending) { + throw new HttpException(`Course is either rejected or is already accepted.`, 406); + } + let updatedCourse = await this.prisma.course.update({ + where: { id: courseId }, + data: { + verificationStatus: CourseVerificationStatus.accepted, + cqfScore: cqf_score + } + }); + return updatedCourse; + } + + async rejectCourse(courseId: number) { + let course = await this.prisma.course.findUnique({ + where: { id: courseId } + }); + if(!course) { + throw new NotFoundException(`Course with id ${courseId} not found`); + } + let updatedCourse = await this.prisma.course.update({ + where: {id: courseId}, + data: {verificationStatus: CourseVerificationStatus.rejected} + }); + return updatedCourse; + } + + async removeCourse(courseId: number) { + let course = await this.prisma.course.findUnique({ + where: { id: courseId } + }); + if(!course) { + throw new NotFoundException(`Course with id ${courseId} not found`); + } + await this.prisma.course.delete({ + where: {id: courseId} + }); + } + + async getTransactions(adminId: number): Promise { + + const walletService = process.env.WALLET_SERVICE_URL; + const endpoint = `${adminId}/transactions/consumers`; + const url = walletService + endpoint; + + try { + const response: AxiosResponse = await axios.get(url); + return response.data; + + } catch (err) { + throw new Error(`Failed to fetch data: ${err.message}`); + } + } + + async addOrRemoveCreditsToProvider(adminId: number, providerId: number, credits: number) { + const walletService = process.env.WALLET_SERVICE_URL; + let endpoint: string; + if(credits >= 0) { + endpoint = `${adminId}/add-credits`; + } else { + endpoint = `${adminId}/reduce-credits`; + } + const url = walletService + endpoint; + const requestBody = { + consumerId: providerId, + credits: credits + }; + try { + const response = await axios.post(url, requestBody); + } catch (err) { + throw new Error(`Failed to send POST request to walletService.`); + } + } + + async editProviderProfile(profileInfo: any) { + + const updatedProfile = await this.prisma.provider.update({ + where: { id: profileInfo.id }, + data: profileInfo + }); + + return updatedProfile; + } + +} + diff --git a/src/admin/dto/CourseResponse.dto.ts b/src/admin/dto/CourseResponse.dto.ts new file mode 100644 index 0000000..768c36f --- /dev/null +++ b/src/admin/dto/CourseResponse.dto.ts @@ -0,0 +1,22 @@ +import { CourseStatus, CourseVerificationStatus } from "@prisma/client"; + +export class CourseResponse { + readonly id: number; + readonly providerId: number; + readonly title: string; + readonly description: string; + readonly courseLink: string; + readonly imgLink: string; + readonly credits: number; + readonly noOfLessons: number; + readonly language: string[]; + readonly duration: number; + readonly competencies: JSON; + readonly author: string; + readonly avgRating: number; + readonly status: CourseStatus; + readonly availabilityTime: Date; + readonly verificationStatus: CourseVerificationStatus; + readonly cqf_score: number; + readonly impact_score: number; +} \ No newline at end of file diff --git a/src/admin/dto/EditProvider.dto.ts b/src/admin/dto/EditProvider.dto.ts new file mode 100644 index 0000000..cbdec15 --- /dev/null +++ b/src/admin/dto/EditProvider.dto.ts @@ -0,0 +1,10 @@ +import { ProviderStatus } from "@prisma/client"; + +export class EditProvider { + readonly id: number; + readonly name: string; + readonly email: string; + readonly walletId: number; + readonly paymentInfo: any; + readonly status: ProviderStatus; +} \ No newline at end of file diff --git a/src/admin/dto/ProviderProfileResponse.dto.ts b/src/admin/dto/ProviderProfileResponse.dto.ts new file mode 100644 index 0000000..619a2b2 --- /dev/null +++ b/src/admin/dto/ProviderProfileResponse.dto.ts @@ -0,0 +1,11 @@ +import { Course, ProviderStatus } from "@prisma/client"; + +export class ProviderProfileResponse { + readonly id: number; + readonly name: string; + readonly email: string; + readonly walletId: number; + readonly paymentInfo: any; + readonly status: ProviderStatus; + // readonly courses: Course[]; +} \ No newline at end of file diff --git a/src/admin/dto/TransactionResponse.dto.ts b/src/admin/dto/TransactionResponse.dto.ts new file mode 100644 index 0000000..57a40a0 --- /dev/null +++ b/src/admin/dto/TransactionResponse.dto.ts @@ -0,0 +1,18 @@ + + +// define in prisma.schema +enum TransactionType { + purchase, + creditRequest, + settlement +} + +export class TransactionResponse { + readonly transactionId: number; + readonly fromId: number; + readonly toId: number; + readonly credits: number; + readonly type: TransactionType; + readonly description?: string; + readonly createdAt: Date; +} \ No newline at end of file diff --git a/src/admin/dto/VerifyCourse.dto.ts b/src/admin/dto/VerifyCourse.dto.ts new file mode 100644 index 0000000..5836367 --- /dev/null +++ b/src/admin/dto/VerifyCourse.dto.ts @@ -0,0 +1,6 @@ +import { CourseVerificationStatus } from "@prisma/client"; + +export class CourseVerify { + readonly cqf_score: number; + readonly verificationStatus: CourseVerificationStatus; +} \ No newline at end of file From 228f84f073207e1e508e06060b76c4a1a13e09a3 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Thu, 26 Oct 2023 18:11:24 +0530 Subject: [PATCH 04/88] provider apis --- prisma/migrations/migration_lock.toml | 3 + prisma/schema.prisma | 44 ++++-- src/app.module.ts | 5 +- src/course/course.service.ts | 71 ++++++++++ src/course/dto/add-course.dto.ts | 95 +++++++++++++ src/course/dto/course.dto.ts | 21 +++ src/provider/dto/feedback.dto.ts | 19 +++ src/provider/dto/login.dto.ts | 23 +++ src/provider/dto/purchase.dto.ts | 14 ++ src/provider/dto/signup.dto.ts | 35 +++++ src/provider/dto/update-profile.dto.ts | 29 ++++ src/provider/dto/view-profile.dto.ts | 12 ++ src/provider/provider.controller.ts | 186 +++++++++++++++++++++++++ src/provider/provider.module.ts | 13 ++ src/provider/provider.service.ts | 164 ++++++++++++++++++++++ 15 files changed, 722 insertions(+), 12 deletions(-) create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 src/course/course.service.ts create mode 100644 src/course/dto/add-course.dto.ts create mode 100644 src/course/dto/course.dto.ts create mode 100644 src/provider/dto/feedback.dto.ts create mode 100644 src/provider/dto/login.dto.ts create mode 100644 src/provider/dto/purchase.dto.ts create mode 100644 src/provider/dto/signup.dto.ts create mode 100644 src/provider/dto/update-profile.dto.ts create mode 100644 src/provider/dto/view-profile.dto.ts create mode 100644 src/provider/provider.controller.ts create mode 100644 src/provider/provider.module.ts create mode 100644 src/provider/provider.service.ts diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a6c4b40..ce99b9c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -35,13 +35,12 @@ enum CourseProgressStatus { completed } - model Admin { id Int @id @default(autoincrement()) name String email String @unique password String - walletId Int + walletId Int @unique } model Provider { @@ -49,10 +48,34 @@ model Provider { name String email String @unique password String - walletId Int + walletId Int @unique paymentInfo Json? status ProviderStatus @default(pending) - courses Course[] + courses Course[] + wallet Wallet @relation(fields: [walletId], references: [walletId]) +} + +// dummy wallet model +model Wallet { + walletId Int @id @default(autoincrement()) + type WalletType + status WalletStatus @default(active) + credits Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + provider Provider? +} + +enum WalletType { + admin + provider + consumer +} + +enum WalletStatus { + active + inactive + frozen } model Course { @@ -63,11 +86,10 @@ model Course { courseLink String imgLink String credits Int - noOfLessons Int + noOfLessons Int? language String duration Int - competency Json[] - competencyIDs Int[] + competency Json[] author String avgRating Float? status CourseStatus @@ -81,12 +103,14 @@ model Course { model UserCourse { id Int @id @default(autoincrement()) - userId String + userId String courseId Int - purchasedAt DateTime - status CourseProgressStatus + purchasedAt DateTime @default(now()) + status CourseProgressStatus @default(inProgress) courseCompletionScore Float? rating Int? feedback String? course Course @relation(fields: [courseId], references: [id]) + + @@unique([userId, courseId]) } diff --git a/src/app.module.ts b/src/app.module.ts index 9046544..bc95a30 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -3,7 +3,7 @@ import { ConfigModule } from "@nestjs/config"; import { AppController } from "./app.controller"; import { AppService } from "./app.service"; import { PrismaModule } from "./prisma/prisma.module"; -import { PrismaService } from "./prisma/prisma.service"; +import { ProviderModule } from "./provider/provider.module"; @Module({ imports: [ @@ -11,8 +11,9 @@ import { PrismaService } from "./prisma/prisma.service"; isGlobal: true, }), PrismaModule, + ProviderModule ], controllers: [AppController], - providers: [AppService, PrismaService], + providers: [AppService], }) export class AppModule {} diff --git a/src/course/course.service.ts b/src/course/course.service.ts new file mode 100644 index 0000000..8984794 --- /dev/null +++ b/src/course/course.service.ts @@ -0,0 +1,71 @@ +import { Injectable } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +import { AddCourseDto } from "./dto/add-course.dto"; +import { CourseProgressStatus } from "@prisma/client"; + +@Injectable() +export class CourseService { + constructor(private prisma: PrismaService) {} + + async addCourse(providerId: number, addCourseDto: AddCourseDto) { + + return this.prisma.course.create({ + data: { + providerId, + ...addCourseDto + } + }) + } + + async getCourse(courseId: number) { + + return this.prisma.course.findUnique({ + where: { + id: courseId + } + }) + } + + async deleteCourse(courseId: number) { + + this.prisma.course.delete({ + where: { + id: courseId + } + }) + } + + async getProviderCourses(providerId: number) { + + return this.prisma.course.findMany({ + where: { + providerId + } + }) + } + + async getUserCourses(courseId: number) { + + return this.prisma.userCourse.findMany({ + where: { + courseId + }, + }) + } + + async markCourseComplete(courseId: number, userId: string) { + + await this.prisma.userCourse.update({ + where: { + userId_courseId: { + courseId, + userId + } + }, + data: { + status: CourseProgressStatus.completed + } + }) + } + +} diff --git a/src/course/dto/add-course.dto.ts b/src/course/dto/add-course.dto.ts new file mode 100644 index 0000000..c60303a --- /dev/null +++ b/src/course/dto/add-course.dto.ts @@ -0,0 +1,95 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { CourseStatus, CourseVerificationStatus } from "@prisma/client"; +import { IsDate, IsInt, IsJSON, IsNotEmpty, IsOptional, IsString, Min } from "class-validator"; + +export class AddCourseDto { + + // course title + @ApiProperty() + @IsNotEmpty() + @IsString() + title: string; + + // description + @ApiProperty() + @IsNotEmpty() + @IsString() + description: string; + + // link for the course content + @ApiProperty() + @IsNotEmpty() + @IsString() + courseLink: string; + + // course image + @ApiProperty() + @IsNotEmpty() + @IsString() + imgLink: string; + + // number of credits required to purchase course + @ApiProperty() + @IsNotEmpty() + @Min(0) + @IsInt() + credits: number; + + // Number of lessons + @ApiProperty() + @IsInt() + @IsOptional() + noOfLessons?: number; + + // language + @ApiProperty() + @IsNotEmpty() + @IsString() + language: string; + + // course duration + @ApiProperty() + @IsNotEmpty() + @Min(0) + @IsInt() + duration: number; + + // competency + @ApiProperty() + @IsNotEmpty() + // @IsJSON() + competency: any[]; + + // author + @ApiProperty() + @IsNotEmpty() + @IsString() + author: string; + + // course status (active/inactive/archived) + @ApiProperty() + @IsNotEmpty() + @IsString() + status: CourseStatus; + + // course availability time + @ApiProperty() + @IsDate() + @IsOptional() + availabilityTime?: Date; +} + +export class AddCourseResponseDto extends AddCourseDto { + + // course ID + readonly id: number; + + // course provider ID + readonly providerId: number; + + // Average rating + readonly avgRating: number; + + // Course Verification Status (pending/accepted/rejected) + readonly verificationStatus: CourseVerificationStatus; +} \ No newline at end of file diff --git a/src/course/dto/course.dto.ts b/src/course/dto/course.dto.ts new file mode 100644 index 0000000..89ab9b7 --- /dev/null +++ b/src/course/dto/course.dto.ts @@ -0,0 +1,21 @@ +import { $Enums, Prisma } from "@prisma/client"; + +export class CourseResponseDto { + + readonly id: number; + readonly providerId: number; + readonly title: string; + readonly description: string; + readonly courseLink: string; + readonly imgLink: string; + readonly credits: number; + readonly noOfLessons: number | null; + readonly language: string; + readonly duration: number; + readonly competency: Prisma.JsonValue[]; + readonly author: string; + readonly avgRating: number | null; + readonly status: $Enums.CourseStatus; + readonly availabilityTime: Date | null; + readonly verificationStatus: $Enums.CourseVerificationStatus; +} \ No newline at end of file diff --git a/src/provider/dto/feedback.dto.ts b/src/provider/dto/feedback.dto.ts new file mode 100644 index 0000000..6b6b1ae --- /dev/null +++ b/src/provider/dto/feedback.dto.ts @@ -0,0 +1,19 @@ + + +export class FeedbackResponseDto { + + // number of consumers that have purchased the course + readonly numberOfPurchases: number + + // list of consumer feedbacks and their ratings + readonly feedbacks: Feedback[] +} + +export interface Feedback { + + // Feedback Text + feedback: string, + + // Integer rating of the course + rating: number +} \ No newline at end of file diff --git a/src/provider/dto/login.dto.ts b/src/provider/dto/login.dto.ts new file mode 100644 index 0000000..778658e --- /dev/null +++ b/src/provider/dto/login.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEmail, IsNotEmpty, IsString } from "class-validator"; + +export class LoginDto { + + // email ID + @ApiProperty() + @IsNotEmpty() + @IsEmail() + email: string + + // password + @ApiProperty() + @IsNotEmpty() + @IsString() + password: string +} + +export class LoginResponseDto { + + // provider ID + readonly providerId: number +} \ No newline at end of file diff --git a/src/provider/dto/purchase.dto.ts b/src/provider/dto/purchase.dto.ts new file mode 100644 index 0000000..52e6288 --- /dev/null +++ b/src/provider/dto/purchase.dto.ts @@ -0,0 +1,14 @@ + + +export class PurchaseResponseDto { + + // user ID + readonly userId: string; + + // course ID + readonly courseId: number; + + // timestamp for when the course was purchased + readonly purchasedAt: Date; +} + diff --git a/src/provider/dto/signup.dto.ts b/src/provider/dto/signup.dto.ts new file mode 100644 index 0000000..4d6ab40 --- /dev/null +++ b/src/provider/dto/signup.dto.ts @@ -0,0 +1,35 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEmail, IsJSON, IsNotEmpty, IsOptional, IsString } from "class-validator"; + +export class SignupDto { + + // name + @ApiProperty() + @IsNotEmpty() + @IsString() + name: string + + // email ID + @ApiProperty() + @IsNotEmpty() + @IsEmail() + email: string + + // password + @ApiProperty() + @IsNotEmpty() + @IsString() + password: string + + // payment info + @ApiProperty() + @IsOptional() + @IsJSON() + paymentInfo?: any +} + +export class SignupResponseDto { + + // provider ID + readonly providerId: number +} \ No newline at end of file diff --git a/src/provider/dto/update-profile.dto.ts b/src/provider/dto/update-profile.dto.ts new file mode 100644 index 0000000..60b3d41 --- /dev/null +++ b/src/provider/dto/update-profile.dto.ts @@ -0,0 +1,29 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEmail, IsJSON, IsOptional, IsString } from "class-validator"; + +export class UpdateProfileDto { + + // name + @ApiProperty() + @IsString() + @IsOptional() + name?: string + + // email ID + @ApiProperty() + @IsEmail() + @IsOptional() + email?: string + + // password + @ApiProperty() + @IsString() + @IsOptional() + password?: string + + // payment info + @ApiProperty() + @IsJSON() + @IsOptional() + paymentInfo?: any +} diff --git a/src/provider/dto/view-profile.dto.ts b/src/provider/dto/view-profile.dto.ts new file mode 100644 index 0000000..b8cd605 --- /dev/null +++ b/src/provider/dto/view-profile.dto.ts @@ -0,0 +1,12 @@ +import { $Enums, Prisma } from "@prisma/client"; + +export class ViewProfileResponseDto { + + readonly id: number; + readonly name: string; + readonly email: string; + readonly password: string; + readonly walletId: number; + readonly paymentInfo: Prisma.JsonValue; + readonly status: $Enums.ProviderStatus; +} diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts new file mode 100644 index 0000000..5fec65a --- /dev/null +++ b/src/provider/provider.controller.ts @@ -0,0 +1,186 @@ +import { Body, Controller, Delete, Get, HttpStatus, Param, ParseIntPipe, Patch, Post, Put, Res } from '@nestjs/common'; +import { ProviderService } from './provider.service'; +import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { SignupDto, SignupResponseDto } from './dto/signup.dto'; +import { LoginDto, LoginResponseDto } from './dto/login.dto'; +import { UpdateProfileDto } from './dto/update-profile.dto'; +import { AddCourseDto, AddCourseResponseDto } from 'src/course/dto/add-course.dto'; +import { ViewProfileResponseDto } from './dto/view-profile.dto'; +import { FeedbackResponseDto } from './dto/feedback.dto'; +import { PurchaseResponseDto } from './dto/purchase.dto'; +import { CourseResponseDto } from 'src/course/dto/course.dto'; + +@Controller('provider') +export class ProviderController { + constructor( + private providerService: ProviderService, + ) {} + + @ApiOperation({ summary: 'create provider account' }) + @ApiResponse({ status: HttpStatus.CREATED, type: SignupResponseDto }) + @Post("/signup") + // create a new provider account + async createAccount( + @Body() signupDto: SignupDto, + @Res() res + ) { + const providerId = await this.providerService.createNewAccount(signupDto); + + res.status(HttpStatus.CREATED).json({ + message: "account created successfully", + data: { + providerId + } + }) + } + + @ApiOperation({ summary: 'provider login' }) + @ApiResponse({ status: HttpStatus.OK, type: LoginResponseDto }) + @Post("/login") + // provider login + async login( + @Body() loginDto: LoginDto, + @Res() res + ) { + const providerId = await this.providerService.getProviderIdFromLogin(loginDto); + + res.status(HttpStatus.OK).json({ + message: "login successful", + data: { + providerId + } + }) + } + + @ApiOperation({ summary: 'view provider profile' }) + @ApiResponse({ status: HttpStatus.OK, type: ViewProfileResponseDto }) + @Get("/:providerId/profile") + // view provider profile information + async viewProfile( + @Param("providerId", ParseIntPipe) providerId: number, + @Res() res + ) { + const provider = await this.providerService.getProvider(providerId); + + res.status(HttpStatus.OK).json({ + message: "fetched successful", + data : provider + }) + } + + @ApiOperation({ summary: 'update provider profile information' }) + @ApiResponse({ status: HttpStatus.OK }) + @Put("/:providerId/profile") + // update provider profile information + async updateProfile( + @Param("providerId", ParseIntPipe) providerId: number, + @Body() updateProfileDto: UpdateProfileDto, + @Res() res + ) { + await this.providerService.updateProfileInfo(providerId, updateProfileDto); + + res.status(HttpStatus.OK).json({ + message: "account updated successfully", + }) + } + + @ApiOperation({ summary: 'add new course' }) + @ApiResponse({ status: HttpStatus.CREATED, type: AddCourseResponseDto }) + @Post("/:providerId/course") + // add new course + async addCourse( + @Param("providerId", ParseIntPipe) providerId: number, + @Body() addCourseDto: AddCourseDto, + @Res() res + ) { + const course = await this.providerService.addNewCourse(providerId, addCourseDto); + + res.status(HttpStatus.CREATED).json({ + message: "course added successfully", + data: course + }) + } + + @ApiOperation({ summary: 'remove a course' }) + @ApiResponse({ status: HttpStatus.OK }) + @Delete("/:providerId/course/:courseId") + // remove an existing course + async removeCourse( + @Param("providerId", ParseIntPipe) providerId: number, + @Param("courseId", ParseIntPipe) courseId: number, + @Res() res + ) { + await this.providerService.removeCourse(providerId, courseId); + + res.status(HttpStatus.OK).json({ + message: "course deleted successfully", + }) + } + + @ApiOperation({ summary: 'View courses offered by self' }) + @ApiResponse({ status: HttpStatus.OK, type: [CourseResponseDto] }) + @Get("/:providerId/course") + // View courses offered by self + async fetchProviderCourses( + @Param("providerId", ParseIntPipe) providerId: number, + @Res() res + ) { + const courses = await this.providerService.getCourses(providerId); + + res.status(HttpStatus.OK).json({ + message: "courses fetched successfully", + data: courses + }) + } + + @ApiOperation({ summary: 'View Course Feedback & ratings, numberOfPurchases' }) + @ApiResponse({ status: HttpStatus.OK, type: FeedbackResponseDto }) + @Get("/:providerId/course/:courseId/feedback") + // View Course Feedback & ratings, numberOfPurchases + async getCourseFeedback( + @Param("providerId", ParseIntPipe) providerId: number, + @Param("courseId", ParseIntPipe) courseId: number, + @Res() res + ) { + const feedbackResponse = await this.providerService.getCourseFeedbacks(providerId, courseId); + + res.status(HttpStatus.OK).json({ + message: "feedbacks fetched successfully", + data: feedbackResponse + }) + } + + @ApiOperation({ summary: 'Get all transactions for course purchase user wise' }) + @ApiResponse({ status: HttpStatus.OK, type: [PurchaseResponseDto] }) + @Get("/:providerId/course/:courseId/purchases") + // Get all the transactions for course purchase user wise + async getCoursePurchases( + @Param("providerId", ParseIntPipe) providerId: number, + @Param("courseId", ParseIntPipe) courseId: number, + @Res() res + ) { + const purchaseResponse = await this.providerService.getCoursePurchases(providerId, courseId); + + res.status(HttpStatus.OK).json({ + message: "purchases fetched successfully", + data: purchaseResponse + }) + } + + @ApiOperation({ summary: 'Mark course as complete' }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:providerId/course/:courseId/completion/:userId") + // Mark course as complete for a user + async markCourseComplete( + @Param("providerId", ParseIntPipe) providerId: number, + @Param("courseId", ParseIntPipe) courseId: number, + @Param("userId") userId: string, + @Res() res + ) { + await this.providerService.markCourseComplete(providerId, courseId, userId); + + res.status(HttpStatus.OK).json({ + message: "course marked complete", + }) + } +} \ No newline at end of file diff --git a/src/provider/provider.module.ts b/src/provider/provider.module.ts new file mode 100644 index 0000000..7431959 --- /dev/null +++ b/src/provider/provider.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { ProviderController } from './provider.controller'; +import { ProviderService } from './provider.service'; +import { CourseService } from 'src/course/course.service'; +import { PrismaModule } from 'src/prisma/prisma.module'; +import { PrismaService } from 'src/prisma/prisma.service'; + +@Module({ + imports: [PrismaModule], + controllers: [ProviderController], + providers: [ProviderService, CourseService, PrismaService] +}) +export class ProviderModule {} \ No newline at end of file diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts new file mode 100644 index 0000000..b2adedc --- /dev/null +++ b/src/provider/provider.service.ts @@ -0,0 +1,164 @@ +import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { SignupDto } from './dto/signup.dto'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { WalletType } from '@prisma/client'; +import { LoginDto } from './dto/login.dto'; +import { UpdateProfileDto } from './dto/update-profile.dto'; +import { AddCourseDto } from 'src/course/dto/add-course.dto'; +import { CourseService } from 'src/course/course.service'; +import { Feedback, FeedbackResponseDto } from './dto/feedback.dto'; +import { PurchaseResponseDto } from './dto/purchase.dto'; +import { CourseResponseDto } from 'src/course/dto/course.dto'; + +@Injectable() +export class ProviderService { + constructor( + private prisma: PrismaService, + private courseService: CourseService + ) {} + + async createNewAccount(signupDto: SignupDto) { + + const provider = await this.prisma.provider.create({ + data: { + ...signupDto, + wallet: { + create: { + type: WalletType.provider, + } + } + } + }) + return provider.id + } + + async getProviderIdFromLogin(loginDto: LoginDto) { + + const provider = await this.prisma.provider.findUnique({ + where: { + email: loginDto.email + } + }) + if(!provider) + throw new NotFoundException("Email ID does not exist"); + + if(provider.password != loginDto.password) + throw new BadRequestException("Incorrect password"); + + return provider.id + } + + async getProvider(providerId: number) { + + const provider = await this.prisma.provider.findUnique({ + where: { + id: providerId + } + }) + if(!provider) + throw new NotFoundException("provider does not exist"); + + return provider + } + + async updateProfileInfo(providerId: number, updateProfileDto: UpdateProfileDto) { + + try { + await this.prisma.provider.update({ + where: { + id: providerId + }, + data: updateProfileDto + }) + } catch { + throw new NotFoundException("profile does not exist"); + } + } + + async addNewCourse(providerId: number, addCourseDto: AddCourseDto) { + + await this.getProvider(providerId); + + return this.courseService.addCourse(providerId, addCourseDto); + } + + async removeCourse(providerId: number, courseId: number) { + + const course = await this.courseService.getCourse(courseId); + if(!course) + throw new NotFoundException("Course does not exist"); + + if(course.providerId != providerId) + throw new BadRequestException("Course does not belong to this provider"); + + return this.courseService.deleteCourse(courseId); + } + + async getCourses(providerId: number): Promise { + + return this.courseService.getProviderCourses(providerId); + } + + async getCourseFeedbacks(providerId: number, courseId: number): Promise { + + const course = await this.courseService.getCourse(courseId); + if(!course) + throw new NotFoundException("Course does not exist"); + + if(course.providerId != providerId) + throw new BadRequestException("Course does not belong to this provider"); + + const userCourses = await this.courseService.getUserCourses(courseId); + + let feedbacks: Feedback[] = []; + for(let u of userCourses) { + if(u.feedback && u.rating) { + feedbacks.push({ + feedback: u.feedback, + rating: u.rating + }) + } + } + return { + numberOfPurchases: userCourses.length, + feedbacks + }; + } + + async getCoursePurchases(providerId: number, courseId: number): Promise { + + const course = await this.courseService.getCourse(courseId); + if(!course) + throw new NotFoundException("Course does not exist"); + + if(course.providerId != providerId) + throw new BadRequestException("Course does not belong to this provider"); + + const userCourses = await this.courseService.getUserCourses(courseId); + + return userCourses.map((u) => { + return { + courseId: u.courseId, + purchasedAt: u.purchasedAt, + userId: u.userId + } + }) + + } + + async markCourseComplete(providerId: number, courseId: number, userId: string) { + + const course = await this.courseService.getCourse(courseId); + if(!course) + throw new NotFoundException("Course does not exist"); + + if(course.providerId != providerId) + throw new BadRequestException("Course does not belong to this provider"); + + try { + await this.courseService.markCourseComplete(courseId, userId); + } catch { + throw new NotFoundException("This user has not subscribed to this course"); + } + } +} \ No newline at end of file From f36cb3db275a0af4e7dbca503cf4f701485a105e Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Fri, 27 Oct 2023 15:32:46 +0530 Subject: [PATCH 05/88] Added credit APIs --- package-lock.json | 8 ++ package.json | 2 + prisma/schema.prisma | 81 ++++++----- prisma/seed.ts | 113 +++++++++++++++ src/admin/admin.controller.spec.ts | 3 + src/admin/admin.controller.ts | 135 +++++++++++++----- src/admin/admin.service.ts | 15 +- ...Response.dto.ts => course-response.dto.ts} | 1 + src/admin/dto/credit-request.dto.ts | 15 ++ ...itProvider.dto.ts => edit-provider.dto.ts} | 0 ...to.ts => provider-profile-response.dto.ts} | 0 ...nse.dto.ts => transaction-response.dto.ts} | 0 ...rifyCourse.dto.ts => verify-course.dto.ts} | 0 13 files changed, 290 insertions(+), 83 deletions(-) create mode 100644 prisma/seed.ts rename src/admin/dto/{CourseResponse.dto.ts => course-response.dto.ts} (99%) create mode 100644 src/admin/dto/credit-request.dto.ts rename src/admin/dto/{EditProvider.dto.ts => edit-provider.dto.ts} (100%) rename src/admin/dto/{ProviderProfileResponse.dto.ts => provider-profile-response.dto.ts} (100%) rename src/admin/dto/{TransactionResponse.dto.ts => transaction-response.dto.ts} (100%) rename src/admin/dto/{VerifyCourse.dto.ts => verify-course.dto.ts} (100%) diff --git a/package-lock.json b/package-lock.json index 254c7d8..a91179c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@prisma/client": "^5.4.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "lodash": "^4.17.21", "nestjs-prisma": "^0.22.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", @@ -29,6 +30,7 @@ "@nestjs/testing": "^9.0.0", "@types/express": "^4.17.13", "@types/jest": "28.1.8", + "@types/lodash": "^4.14.200", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", @@ -2182,6 +2184,12 @@ "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.200", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz", + "integrity": "sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz", diff --git a/package.json b/package.json index ae07aee..9b0aa10 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@prisma/client": "^5.4.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "lodash": "^4.17.21", "nestjs-prisma": "^0.22.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", @@ -47,6 +48,7 @@ "@nestjs/testing": "^9.0.0", "@types/express": "^4.17.13", "@types/jest": "28.1.8", + "@types/lodash": "^4.14.200", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f1c0438..ff2c213 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -41,57 +41,56 @@ enum TransactionType { settlement } - model Admin { - id Int @id @default(autoincrement()) - name String - email String @unique - password String - walletId Int + id Int @id @default(autoincrement()) + name String + email String @unique + password String + walletId Int } model Provider { - id Int @id @default(autoincrement()) - name String - email String @unique - password String + id Int @id @default(autoincrement()) + name String + email String @unique + password String walletId Int paymentInfo Json? - status ProviderStatus @default(pending) - courses Course[] + status ProviderStatus @default(pending) + courses Course[] } model Course { - id Int @id @default(autoincrement()) - providerId Int - title String - description String - courseLink String - imgLink String - credits Int - noOfLessons Int - language String[] - duration Int - competency Json - author String - avgRating Float? - status CourseStatus - availabilityTime DateTime? - verificationStatus CourseVerificationStatus @default(pending) - cqfScore Int? - impactScore Float? - provider Provider @relation(fields: [providerId], references: [id]) - userCourses UserCourse[] + id Int @id @default(autoincrement()) + providerId Int + title String + description String + courseLink String + imgLink String + credits Int + noOfLessons Int + language String[] + duration Int + competency Json + author String + avgRating Float? + status CourseStatus @default(pending) + availabilityTime DateTime? + verificationStatus CourseVerificationStatus @default(pending) + cqfScore Int? + impactScore Float? + provider Provider @relation(fields: [providerId], references: [id]) + UserCourse UserCourse[] } model UserCourse { - id Int @id @default(autoincrement()) - userId String - courseId Int - purchasedAt DateTime - status CourseProgressStatus - courseCompletionScore Float? - rating Int? - feedback String? - course Course @relation(fields: [courseId], references: [id]) + id Int @id @default(autoincrement()) + userId String + courseId Int + purchasedAt DateTime + status CourseProgressStatus + courseCompletionScore Float? + rating Int? + feedback String? + course Course @relation(fields: [courseId], references: [id]) } diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..f43e854 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,113 @@ +import { CourseProgressStatus, CourseStatus, CourseVerificationStatus, PrismaClient } from '@prisma/client'; + +// initialize Prisma Client +const prisma = new PrismaClient(); + +async function main() { + // create dummy data + + const admin1 = await prisma.admin.upsert({ + where: { id: 1 }, + update: {}, + create: { + id: 1, + name: 'admin1', + email: "admin1@gmail.com", + password: "123456", + walletId: 2 + }, + }); + + const admin2 = await prisma.admin.upsert({ + where: { id: 2 }, + update: {}, + create: { + id: 2, + name: 'admin2', + email: "admin2@gmail.com", + password: "admin", + walletId: 3 + }, + }); + + const provider1 = await prisma.provider.upsert({ + where: { id: 1 }, + update: {}, + create: { + id: 1, + name: "udemy", + email: "udemyorg@gmail.in", + password: "Udemy@9812", + walletId: 4, + paymentInfo: { + bankAccNo: "1111111111", + otherDetails: { + + } + }, + status: 'verified', + // courses: [] + } + }); + + const provider2 = await prisma.provider.upsert({ + where: { id: 2 }, + update: {}, + create: { + id: 2, + name: "coursera", + email: "coursera@gmail.in", + password: "Coursera@999", + walletId: 5, + paymentInfo: { + bankAccNo: "1111111113", + otherDetails: { + + } + }, + status: 'pending', + // courses: [] + } + }); + + const course1 = prisma.course.create({ + data: { + id: 1, + providerId: 1, + title: "Learn DevOps & Kubernetes", + description: "This course enables anyone to get started with devops engineering.", + courseLink: "https://udemy.com/courses/pYUxbhj", + imgLink: "https://udemy.com/courses/pYUxbhj/images/cover1.jpg", + credits: 120, + noOfLessons: 120, + language: ["english", "hindi"], + duration: 48, + competency: { + 9: ["Level1", "Level3"], + 11: ["Level1"], + 14: [ "Level5" ] + }, + author: "Jason Frig", + status: CourseStatus.active, + availabilityTime: new Date("2023-26-01T12:00:00"), + verificationStatus: CourseVerificationStatus.accepted, + cqfScore: 10, + } + }); + + const resp = await prisma.course.findMany({}); + console.log("All courses: ", resp); + + console.log({ admin1, admin2, provider1, provider2, course1 }); +} + +// execute the main function +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + // close Prisma Client at the end + await prisma.$disconnect(); + }); \ No newline at end of file diff --git a/src/admin/admin.controller.spec.ts b/src/admin/admin.controller.spec.ts index c854e7e..baf19ac 100644 --- a/src/admin/admin.controller.spec.ts +++ b/src/admin/admin.controller.spec.ts @@ -4,9 +4,11 @@ import { AdminService } from './admin.service'; import { PrismaModule } from 'nestjs-prisma'; import { PrismaService } from '../prisma/prisma.service'; import { ConfigService } from '@nestjs/config'; +import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; describe('AdminController', () => { let controller: AdminController; + let adminService: AdminService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -16,6 +18,7 @@ describe('AdminController', () => { }).compile(); controller = module.get(AdminController); + adminService = module.get(AdminService); }); it('should be defined', () => { diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 2bd4e8f..057ddc1 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -1,15 +1,17 @@ import { Controller, Body, Get, Post, Patch, Res, Delete, HttpStatus, Param, ParseIntPipe, Logger} from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { AdminService } from './admin.service'; -import { ProviderProfileResponse } from './dto/ProviderProfileResponse.dto'; +import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; import { getPrismaErrorStatusAndMessage } from '../utils/utils'; -import { EditProvider } from './dto/EditProvider.dto'; -import { Course } from '@prisma/client'; -import { CourseResponse } from './dto/CourseResponse.dto'; -import { CourseVerify } from './dto/VerifyCourse.dto'; -import { TransactionResponse } from './dto/TransactionResponse.dto'; - -@Controller('/api/admin') +import { EditProvider } from './dto/edit-provider.dto'; +import { CourseResponse } from './dto/course-response.dto'; +import { CourseVerify } from './dto/verify-course.dto'; +import { TransactionResponse } from './dto/transaction-response.dto'; +import { Response } from 'express'; +import { CreditRequest } from './dto/credit-request.dto'; +import { json } from 'stream/consumers'; + +@Controller('admin') @ApiTags('admin') export class AdminController { private readonly logger = new Logger(AdminController.name); @@ -19,7 +21,7 @@ export class AdminController { @ApiOperation({ summary: "Get all providers" }) @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse, isArray: true}) @Get('/providers') - async getAllProviders(@Res() res): Promise { + async getAllProviders(@Res() res : Response) { try { this.logger.log(`Getting information of all the providers`); @@ -27,7 +29,7 @@ export class AdminController { this.logger.log(`Successfully retrieved all the providers`); - return res.status(HttpStatus.OK).json({ + res.status(HttpStatus.OK).json({ message: "All providers fetched", data: providers }); @@ -35,7 +37,7 @@ export class AdminController { this.logger.error(`Failed to retreive all the providers' information`); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - return res.status(statusCode).json({ + res.status(statusCode).json({ statusCode, message: errorMessage || "Failed to fetch all the providers' information", }); @@ -46,8 +48,8 @@ export class AdminController { @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse}) @Get('/providers/:providerId') async getProviderProfile ( - @Param("providerId", ParseIntPipe) providerId: number, @Res() res - ) : Promise{ + @Param("providerId", ParseIntPipe) providerId: number, @Res() res: Response + ) { try { this.logger.log(`Getting provider information for id ${providerId}`); @@ -55,7 +57,7 @@ export class AdminController { this.logger.log(`Successfully retrieved the provider profile information`); - return res.status(HttpStatus.OK).json({ + res.status(HttpStatus.OK).json({ message: "Provider profile retrieved successfully", data: provider }); @@ -63,7 +65,7 @@ export class AdminController { this.logger.error(`Failed to retrieve the provider profile information`); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - return res.status(statusCode).json({ + res.status(statusCode).json({ statusCode, message: errorMessage || "Failed to retrieve the profile information for the given providerId", }); @@ -76,7 +78,7 @@ export class AdminController { @Patch('/providers/:providerId') async editProviderProfile ( @Param("providerId", ParseIntPipe) providerId: number, @Body() providerDto: EditProvider ,@Res() res - ) : Promise{ + ){ try { this.logger.log(`Getting provider information for id ${providerId}`); @@ -93,7 +95,7 @@ export class AdminController { this.logger.log(`Successfully retrieved the provider profile information`); - return res.status(HttpStatus.OK).json({ + res.status(HttpStatus.OK).json({ message: "Provider profile retrieved successfully", data: updatedProfile }); @@ -101,7 +103,7 @@ export class AdminController { this.logger.error(`Failed to retrieve the provider profile information`); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - return res.status(statusCode).json({ + res.status(statusCode).json({ statusCode, message: errorMessage || "Failed to retrieve the profile information for the given providerId", }); @@ -110,8 +112,8 @@ export class AdminController { @ApiOperation({ summary: "Get all the courses"}) @ApiResponse({ status: HttpStatus.OK, type: CourseResponse, isArray: true}) - @Patch('/courses/') - async getAllCoursess(@Res() res) : Promise{ + @Get('/courses/') + async getAllCourses(@Res() res){ try { this.logger.log(`Fetching all courses on marketplace (verified, pending & rejected)`); @@ -119,7 +121,7 @@ export class AdminController { this.logger.log(`Successfully retrieved all the courses`); - return res.status(HttpStatus.OK).json({ + res.status(HttpStatus.OK).json({ message: "All courses retrieved successfully", data: courses }); @@ -127,7 +129,7 @@ export class AdminController { this.logger.error(`Failed to retrieve all courses`); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - return res.status(statusCode).json({ + res.status(statusCode).json({ statusCode, message: errorMessage || "Failed to retrieve all courses", }); @@ -139,7 +141,7 @@ export class AdminController { @Get('/course/:courseId') async getCourseById ( @Param("courseId", ParseIntPipe) courseId: number, @Res() res - ) : Promise{ + ){ try { this.logger.log(`Getting course information for id ${courseId}`); @@ -147,7 +149,7 @@ export class AdminController { this.logger.log(`Successfully retrieved the course`); - return res.status(HttpStatus.OK).json({ + res.status(HttpStatus.OK).json({ message: "Course retrieved successfully", data: course }); @@ -155,7 +157,7 @@ export class AdminController { this.logger.error(`Failed to retrieve the course for the courseId ${courseId}`); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - return res.status(statusCode).json({ + res.status(statusCode).json({ statusCode, message: errorMessage || `Failed to retrieve the course with id ${courseId}`, }); @@ -168,7 +170,7 @@ export class AdminController { @Patch('/course/:courseId/accept') async acceptCourse ( @Param("courseId", ParseIntPipe) courseId: number, @Body() verifyBody: CourseVerify, @Res() res - ) : Promise{ + ) { try { this.logger.log(`Verifying the course with id ${courseId}`); @@ -176,7 +178,7 @@ export class AdminController { this.logger.log(`Successfully accepted the course`); - return res.status(HttpStatus.OK).json({ + res.status(HttpStatus.OK).json({ message: "Course accepted successfully", data: course }); @@ -184,7 +186,7 @@ export class AdminController { this.logger.error(`Failed to accept the course for the courseId ${courseId}`); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - return res.status(statusCode).json({ + res.status(statusCode).json({ statusCode, message: errorMessage || `Failed to accept the course with id ${courseId}`, }); @@ -196,7 +198,7 @@ export class AdminController { @Patch('/course/:courseId/reject') async rejectCourse ( @Param("courseId", ParseIntPipe) courseId: number, @Res() res - ) : Promise{ + ) { try { this.logger.log(`Processing reject request of course with id ${courseId}`); @@ -204,7 +206,7 @@ export class AdminController { this.logger.log(`Successfully rejected the course`); - return res.status(HttpStatus.OK).json({ + res.status(HttpStatus.OK).json({ message: "Course rejected successfully", data: course }); @@ -212,7 +214,7 @@ export class AdminController { this.logger.error(`Failed to reject the course with the courseId ${courseId}`); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - return res.status(statusCode).json({ + res.status(statusCode).json({ statusCode, message: errorMessage || `Failed to reject the course with id ${courseId}`, }); @@ -224,7 +226,7 @@ export class AdminController { @Delete('/course/:courseId') async removeCourse ( @Param("courseId", ParseIntPipe) courseId: number, @Res() res - ) : Promise{ + ) { try { this.logger.log(`Processing removal request of course with id ${courseId}`); @@ -232,7 +234,7 @@ export class AdminController { this.logger.log(`Successfully deleted the course`); - return res.status(HttpStatus.OK).json({ + res.status(HttpStatus.OK).json({ message: "Course deleted successfully", data: course }); @@ -240,7 +242,7 @@ export class AdminController { this.logger.error(`Failed to delete the course with the courseId ${courseId}`); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - return res.status(statusCode).json({ + res.status(statusCode).json({ statusCode, message: errorMessage || `Failed to delete the course with id ${courseId}`, }); @@ -253,7 +255,7 @@ export class AdminController { @ApiResponse({ status: HttpStatus.OK, type: TransactionResponse, isArray: true}) @Get('/:adminId/transactions/consumers') async getTransactions (@Param("adminId") adminId: number, @Res() res - ) : Promise{ + ){ try { this.logger.log(`Getting all transactions between admin and consumers.`); @@ -261,7 +263,7 @@ export class AdminController { this.logger.log(`Successfully fetched all the transactions between admin and consumers`); - return res.status(HttpStatus.OK).json({ + res.status(HttpStatus.OK).json({ message: "Fetched admin-consumers transactions", data: transactions }); @@ -269,11 +271,70 @@ export class AdminController { this.logger.error(`Failed to fetch the transactions between admin and consumers`); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - return res.status(statusCode).json({ + res.status(statusCode).json({ statusCode, message: errorMessage || `Failed to fetch the transactions between admin and consumers`, }); } } + @ApiOperation({ summary: "Add credits to a Provider"}) + @ApiResponse({ status: HttpStatus.OK, type: json}) + @Post('/:adminId/providers/credits/addCredits') + async addCredits (@Param("adminId") adminId: number, @Body() reqBody: CreditRequest, @Res() res + ){ + try { + this.logger.log(`Adding credits to providers' wallet`); + + const providerId = reqBody.providerId; + const credits = reqBody.credits; + + const provider = this.adminService.addOrRemoveCreditsToProvider(adminId, providerId, credits); + + this.logger.log(`Succesfully Added credits to providers' wallet.`); + + res.status(HttpStatus.OK).json({ + message: "Added credits to provider", + data: provider + }); + } catch (err) { + this.logger.error(`Failed to add credits to the provider`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to add credits to the provider`, + }); + } + } + + @ApiOperation({ summary: "Remove credits from a Provider"}) + @ApiResponse({ status: HttpStatus.OK, type: json }) + @Post('/:adminId/providers/credits/reduceCredits') + async reduceCredits (@Param("adminId") adminId: number, @Body() reqBody: CreditRequest, @Res() res + ){ + try { + this.logger.log(`Reducing credits from providers' wallet`); + + const providerId = reqBody.providerId; + const credits = reqBody.credits; + + const provider = this.adminService.addOrRemoveCreditsToProvider(adminId, providerId, credits); + + this.logger.log(`Succesfully reduced credits from providers' wallet.`); + + res.status(HttpStatus.OK).json({ + message: "Removed credits from provider", + data: provider + }); + } catch (err) { + this.logger.error(`Failed to remove credits from the provider`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || `Failed to remove credits from the provider`, + }); + } + } } diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 2e52dd8..c0eecbe 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -3,6 +3,8 @@ import { HttpException, Injectable, NotFoundException } from '@nestjs/common'; import axios, { AxiosResponse } from 'axios'; import { PrismaService } from '../prisma/prisma.service'; import { Provider, ProviderStatus, Course, CourseVerificationStatus } from '@prisma/client'; +import { omit } from 'lodash'; +import { EditProvider } from './dto/edit-provider.dto'; @Injectable() export class AdminService { @@ -29,8 +31,10 @@ export class AdminService { return updatedRecord; } - async findAllProviders(): Promise { - let providers = await this.prisma.provider.findMany(); + async findAllProviders(): Promise[]> { + let providers = await this.prisma.provider.findMany({ + select: { id: true, name: true, email: true, walletId: true, paymentInfo: true, courses: true, status: true} + }); return providers; } @@ -109,7 +113,7 @@ export class AdminService { async getTransactions(adminId: number): Promise { - const walletService = process.env.WALLET_SERVICE_URL; + const walletService = process.env.MOCK_WALLET_SERVICE_URL; const endpoint = `${adminId}/transactions/consumers`; const url = walletService + endpoint; @@ -123,7 +127,7 @@ export class AdminService { } async addOrRemoveCreditsToProvider(adminId: number, providerId: number, credits: number) { - const walletService = process.env.WALLET_SERVICE_URL; + const walletService = process.env.MOCK_WALLET_SERVICE_URL; let endpoint: string; if(credits >= 0) { endpoint = `${adminId}/add-credits`; @@ -137,12 +141,13 @@ export class AdminService { }; try { const response = await axios.post(url, requestBody); + return response; } catch (err) { throw new Error(`Failed to send POST request to walletService.`); } } - async editProviderProfile(profileInfo: any) { + async editProviderProfile(profileInfo: EditProvider) { const updatedProfile = await this.prisma.provider.update({ where: { id: profileInfo.id }, diff --git a/src/admin/dto/CourseResponse.dto.ts b/src/admin/dto/course-response.dto.ts similarity index 99% rename from src/admin/dto/CourseResponse.dto.ts rename to src/admin/dto/course-response.dto.ts index 768c36f..e56f457 100644 --- a/src/admin/dto/CourseResponse.dto.ts +++ b/src/admin/dto/course-response.dto.ts @@ -1,6 +1,7 @@ import { CourseStatus, CourseVerificationStatus } from "@prisma/client"; export class CourseResponse { + readonly id: number; readonly providerId: number; readonly title: string; diff --git a/src/admin/dto/credit-request.dto.ts b/src/admin/dto/credit-request.dto.ts new file mode 100644 index 0000000..2b69949 --- /dev/null +++ b/src/admin/dto/credit-request.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEmail, IsJSON, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; + +export class CreditRequest { + + @ApiProperty() + @IsNotEmpty() + @IsNumber() + readonly providerId: number + + @ApiProperty() + @IsNotEmpty() + @IsNumber() + readonly credits: number +} \ No newline at end of file diff --git a/src/admin/dto/EditProvider.dto.ts b/src/admin/dto/edit-provider.dto.ts similarity index 100% rename from src/admin/dto/EditProvider.dto.ts rename to src/admin/dto/edit-provider.dto.ts diff --git a/src/admin/dto/ProviderProfileResponse.dto.ts b/src/admin/dto/provider-profile-response.dto.ts similarity index 100% rename from src/admin/dto/ProviderProfileResponse.dto.ts rename to src/admin/dto/provider-profile-response.dto.ts diff --git a/src/admin/dto/TransactionResponse.dto.ts b/src/admin/dto/transaction-response.dto.ts similarity index 100% rename from src/admin/dto/TransactionResponse.dto.ts rename to src/admin/dto/transaction-response.dto.ts diff --git a/src/admin/dto/VerifyCourse.dto.ts b/src/admin/dto/verify-course.dto.ts similarity index 100% rename from src/admin/dto/VerifyCourse.dto.ts rename to src/admin/dto/verify-course.dto.ts From ee028acb760b1ed49cdc790fcfb343f156cc208e Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Mon, 30 Oct 2023 16:41:09 +0530 Subject: [PATCH 06/88] init --- prisma/schema.prisma | 106 ++++++++++++++++++++++++++++++-- src/course/course.controller.ts | 77 +++++++++++++++++++++++ src/course/course.module.ts | 8 +++ src/course/course.service.ts | 73 ++++++++++++++++++++++ src/course/dto/course.dto.ts | 21 +++++++ src/course/dto/feedback.dto.ts | 17 +++++ src/course/dto/search.dto.ts | 15 +++++ 7 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 src/course/course.controller.ts create mode 100644 src/course/course.module.ts create mode 100644 src/course/course.service.ts create mode 100644 src/course/dto/course.dto.ts create mode 100644 src/course/dto/feedback.dto.ts create mode 100644 src/course/dto/search.dto.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e1c9ebf..befa61b 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,9 +12,105 @@ datasource db { // Here goes models -//dummy user model -model User { - id Int @id @default(autoincrement()) - email String @unique - name String? +enum ProviderStatus { + pending + verified + rejected } + +enum CourseVerificationStatus { + pending + accepted + rejected +} + +enum CourseStatus { + active + inactive + archived +} + +enum CourseProgressStatus { + inProgress + completed +} + +model Admin { + id Int @id @default(autoincrement()) + name String + email String @unique + password String + walletId Int @unique +} + +model Provider { + id Int @id @default(autoincrement()) + name String + email String @unique + password String + walletId Int @unique + paymentInfo Json? + status ProviderStatus @default(pending) + courses Course[] + wallet Wallet @relation(fields: [walletId], references: [walletId]) +} + +// dummy wallet model +model Wallet { + walletId Int @id @default(autoincrement()) + type WalletType + status WalletStatus @default(active) + credits Int @default(0) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + provider Provider? +} + +enum WalletType { + admin + provider + consumer +} + +enum WalletStatus { + active + inactive + frozen +} + +model Course { + id Int @id @default(autoincrement()) + providerId Int + title String + description String + courseLink String + imgLink String + credits Int + noOfLessons Int? + language String + duration Int + competency Json[] + author String + avgRating Float? + status CourseStatus + availabilityTime DateTime? + verificationStatus CourseVerificationStatus @default(pending) + cqfScore Int? + impactScore Float? + provider Provider @relation(fields: [providerId], references: [id]) + userCourses UserCourse[] +} + +model UserCourse { + id Int @id @default(autoincrement()) + userId String + courseId Int + purchasedAt DateTime @default(now()) + status CourseProgressStatus @default(inProgress) + courseCompletionScore Float? + rating Int? + feedback String? + course Course @relation(fields: [courseId], references: [id]) + + @@unique([userId, courseId]) +} \ No newline at end of file diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts new file mode 100644 index 0000000..6179198 --- /dev/null +++ b/src/course/course.controller.ts @@ -0,0 +1,77 @@ +import { Body, Controller, Get, HttpStatus, Param, ParseIntPipe, Patch, Post, Res } from "@nestjs/common"; +import { ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { CourseService } from "./course.service"; +import { SearchDto } from "./dto/search.dto"; +import { CourseDto } from "./dto/course.dto"; +import { FeedbackDto } from "./dto/feedback.dto"; + +@Controller('course') +export class CourseController { + constructor(private readonly courseService: CourseService) {} + + @ApiOperation({ summary: 'Search courses' }) + @ApiResponse({ status: HttpStatus.OK, type: [CourseDto] }) + @Post("/search") + // search courses + async searchCourses( + @Body() searchDto: SearchDto, + @Res() res + ) { + const courses = await this.courseService.searchCourses(searchDto); + + res.status(HttpStatus.OK).json({ + message: "search successful", + data: courses + }) + } + + @ApiOperation({ summary: 'Fetch details of one course' }) + @ApiResponse({ status: HttpStatus.OK, type: CourseDto }) + @Get("/:courseId") + // Fetch details of one course + async getCourse( + @Param("courseId", ParseIntPipe) courseId: number, + @Res() res + ) { + const course = await this.courseService.getOneCourse(courseId); + + res.status(HttpStatus.OK).json({ + message: "fetch successful", + data: course + }) + } + + @ApiOperation({ summary: 'Confirmation of user purchase of a course' }) + @ApiResponse({ status: HttpStatus.OK }) + @Post("/:courseId/purchase/:userId") + // Confirmation of user purchase of a course + async purchaseCourse( + @Param("courseId", ParseIntPipe) courseId: number, + @Param("userId") userId: string, + @Res() res + ) { + await this.courseService.insertUserCourse(courseId, userId); + + res.status(HttpStatus.OK).json({ + message: "purchase successful" + }) + } + + @ApiOperation({ summary: 'Give feedback and rating' }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:courseId/purchase/:userId") + // Give feedback and rating + async feedback( + @Param("courseId", ParseIntPipe) courseId: number, + @Param("userId") userId: string, + @Body() feedbackDto: FeedbackDto, + @Res() res + ) { + await this.courseService.giveCourseFeedback(courseId, userId, feedbackDto); + + res.status(HttpStatus.OK).json({ + message: "feedback successful" + }) + } + +} diff --git a/src/course/course.module.ts b/src/course/course.module.ts new file mode 100644 index 0000000..99f66b0 --- /dev/null +++ b/src/course/course.module.ts @@ -0,0 +1,8 @@ +import { Module } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; + +@Module({ + controllers: [], + providers: [PrismaService], +}) +export class CourseModule {} diff --git a/src/course/course.service.ts b/src/course/course.service.ts new file mode 100644 index 0000000..8d1f4cb --- /dev/null +++ b/src/course/course.service.ts @@ -0,0 +1,73 @@ +import { Injectable, NotFoundException } from "@nestjs/common"; +import { PrismaService } from "src/prisma/prisma.service"; +import { SearchDto } from "./dto/search.dto"; +import { CourseDto } from "./dto/course.dto"; +import { FeedbackDto } from "./dto/feedback.dto"; + +@Injectable() +export class CourseService { + constructor( + private prisma: PrismaService, + ) {} + + async searchCourses(searchDto: SearchDto): Promise { + + const courses = await this.prisma.course.findMany({ + where: { + title: { + contains: searchDto.searchInput, + mode: "insensitive", + } + } + }) + + + return courses; + } + + async getOneCourse(courseId: number): Promise { + + const course = await this.prisma.course.findUnique({ + where: { + id: courseId + } + }) + if(!course) + throw new NotFoundException("Course does not exist"); + + return course; + } + + async insertUserCourse(courseId: number, userId: string) { + + await this.getOneCourse(courseId); + + await this.prisma.userCourse.create({ + data: { + courseId, + userId + } + }) + } + + async giveCourseFeedback(courseId: number, userId: string, feedbackDto: FeedbackDto) { + + await this.getOneCourse(courseId); + + try { + await this.prisma.userCourse.update({ + where: { + userId_courseId: { + courseId, + userId + } + }, + data: { + ...feedbackDto + } + }); + } catch { + throw new NotFoundException("This user has not subscribed to this course"); + } + } +} diff --git a/src/course/dto/course.dto.ts b/src/course/dto/course.dto.ts new file mode 100644 index 0000000..123f24b --- /dev/null +++ b/src/course/dto/course.dto.ts @@ -0,0 +1,21 @@ +import { $Enums, Prisma } from "@prisma/client"; + +export class CourseDto { + + readonly id: number; + readonly providerId: number; + readonly title: string; + readonly description: string; + readonly courseLink: string; + readonly imgLink: string; + readonly credits: number; + readonly noOfLessons: number | null; + readonly language: string; + readonly duration: number; + readonly competency: Prisma.JsonValue[]; + readonly author: string; + readonly avgRating: number | null; + readonly status: $Enums.CourseStatus; + readonly availabilityTime: Date | null; + readonly verificationStatus: $Enums.CourseVerificationStatus; +} \ No newline at end of file diff --git a/src/course/dto/feedback.dto.ts b/src/course/dto/feedback.dto.ts new file mode 100644 index 0000000..8b18b8b --- /dev/null +++ b/src/course/dto/feedback.dto.ts @@ -0,0 +1,17 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsInt, IsNotEmpty, IsString } from "class-validator"; + +export class FeedbackDto { + + // Feedback Text + @ApiProperty() + @IsNotEmpty() + @IsString() + feedback: string; + + // Integer rating of the course + @ApiProperty() + @IsNotEmpty() + @IsInt() + rating: number; +} diff --git a/src/course/dto/search.dto.ts b/src/course/dto/search.dto.ts new file mode 100644 index 0000000..cbba432 --- /dev/null +++ b/src/course/dto/search.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsString } from "class-validator"; + +export class SearchDto { + + // Search string + @ApiProperty() + @IsNotEmpty() + @IsString() + searchInput: string; + + // competency filters + @ApiProperty() + competencies: Record; +} From 2ac5cd66fa3ae6fa6140131789c52d8fe07c4c1c Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Mon, 30 Oct 2023 17:06:47 +0530 Subject: [PATCH 07/88] Debug --- package-lock.json | 25 ++++++++-- package.json | 5 +- prisma/schema.prisma | 4 +- prisma/seed.ts | 6 +-- src/admin/admin.controller.ts | 16 +++--- src/admin/admin.module.ts | 6 ++- src/admin/admin.service.ts | 49 +++++++++++-------- src/admin/dto/edit-provider.dto.ts | 11 ++--- .../dto/provider-profile-response.dto.ts | 29 ++++++++--- src/app.module.ts | 2 + .../mock-wallet.controller.spec.ts | 18 +++++++ src/mock-wallet/mock-wallet.controller.ts | 4 ++ src/mock-wallet/mock-wallet.module.ts | 9 ++++ src/mock-wallet/mock-wallet.service.spec.ts | 18 +++++++ src/mock-wallet/mock-wallet.service.ts | 49 +++++++++++++++++++ 15 files changed, 193 insertions(+), 58 deletions(-) create mode 100644 src/mock-wallet/mock-wallet.controller.spec.ts create mode 100644 src/mock-wallet/mock-wallet.controller.ts create mode 100644 src/mock-wallet/mock-wallet.module.ts create mode 100644 src/mock-wallet/mock-wallet.service.spec.ts create mode 100644 src/mock-wallet/mock-wallet.service.ts diff --git a/package-lock.json b/package-lock.json index a91179c..84dc5b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@nestjs/config": "^3.1.1", "@nestjs/core": "^9.0.0", "@nestjs/platform-express": "^9.0.0", - "@nestjs/swagger": "^7.1.11", + "@nestjs/swagger": "^7.1.14", "@prisma/client": "^5.4.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -22,7 +22,8 @@ "nestjs-prisma": "^0.22.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "swagger-ui-express": "^5.0.0" }, "devDependencies": { "@nestjs/cli": "^9.0.0", @@ -1715,9 +1716,9 @@ } }, "node_modules/@nestjs/swagger": { - "version": "7.1.13", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.13.tgz", - "integrity": "sha512-aHfW0rDZZKTuPVSkxutBCB16lBy5vrsHVoRF5RvPtH7U2cm4Vf+OnfhxKKuG2g2Xocn9sDL+JAyVlY2VN3ytTw==", + "version": "7.1.14", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.14.tgz", + "integrity": "sha512-2Ol4S6qHeYVVmkshkWBM8E/qkmEqEOUj2QIewr0jLSyo30H7f3v81pJyks6pTLy4PK0LGUXojMvIfFIE3mmGQQ==", "dependencies": { "@nestjs/mapped-types": "2.0.2", "js-yaml": "4.1.0", @@ -7585,6 +7586,20 @@ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.9.0.tgz", "integrity": "sha512-NUHSYoe5XRTk/Are8jPJ6phzBh3l9l33nEyXosM17QInoV95/jng8+PuSGtbD407QoPf93MH3Bkh773OgesJpA==" }, + "node_modules/swagger-ui-express": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.0.tgz", + "integrity": "sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", diff --git a/package.json b/package.json index 9b0aa10..35302d5 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "@nestjs/config": "^3.1.1", "@nestjs/core": "^9.0.0", "@nestjs/platform-express": "^9.0.0", - "@nestjs/swagger": "^7.1.11", + "@nestjs/swagger": "^7.1.14", "@prisma/client": "^5.4.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -40,7 +40,8 @@ "nestjs-prisma": "^0.22.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", - "rxjs": "^7.2.0" + "rxjs": "^7.2.0", + "swagger-ui-express": "^5.0.0" }, "devDependencies": { "@nestjs/cli": "^9.0.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ff2c213..9324e8f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -74,9 +74,9 @@ model Course { competency Json author String avgRating Float? - status CourseStatus @default(pending) + status CourseStatus availabilityTime DateTime? - verificationStatus CourseVerificationStatus @default(pending) + verificationStatus CourseVerificationStatus cqfScore Int? impactScore Float? provider Provider @relation(fields: [providerId], references: [id]) diff --git a/prisma/seed.ts b/prisma/seed.ts index f43e854..305805e 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,4 +1,4 @@ -import { CourseProgressStatus, CourseStatus, CourseVerificationStatus, PrismaClient } from '@prisma/client'; +import { CourseStatus, CourseVerificationStatus, PrismaClient } from '@prisma/client'; // initialize Prisma Client const prisma = new PrismaClient(); @@ -70,7 +70,7 @@ async function main() { } }); - const course1 = prisma.course.create({ + const course1 = await prisma.course.create({ data: { id: 1, providerId: 1, @@ -89,7 +89,7 @@ async function main() { }, author: "Jason Frig", status: CourseStatus.active, - availabilityTime: new Date("2023-26-01T12:00:00"), + availabilityTime: new Date("2023-06-01"), verificationStatus: CourseVerificationStatus.accepted, cqfScore: 10, } diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 057ddc1..6f6180f 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -86,6 +86,7 @@ export class AdminController { id: providerDto.id, name: providerDto.name, email: providerDto.email, + password: providerDto.password, paymentInfo: providerDto.paymentInfo, walletId: providerDto.walletId, status: providerDto.status @@ -145,7 +146,7 @@ export class AdminController { try { this.logger.log(`Getting course information for id ${courseId}`); - const course = this.adminService.findCourseById(courseId); + const course = await this.adminService.findCourseById(courseId); this.logger.log(`Successfully retrieved the course`); @@ -174,7 +175,7 @@ export class AdminController { try { this.logger.log(`Verifying the course with id ${courseId}`); - const course = this.adminService.acceptCourse(courseId, verifyBody.cqf_score); + const course = await this.adminService.acceptCourse(courseId, verifyBody.cqf_score); this.logger.log(`Successfully accepted the course`); @@ -202,7 +203,7 @@ export class AdminController { try { this.logger.log(`Processing reject request of course with id ${courseId}`); - const course = this.adminService.rejectCourse(courseId); + const course = await this.adminService.rejectCourse(courseId); this.logger.log(`Successfully rejected the course`); @@ -230,7 +231,7 @@ export class AdminController { try { this.logger.log(`Processing removal request of course with id ${courseId}`); - const course = this.adminService.removeCourse(courseId); + const course = await this.adminService.removeCourse(courseId); this.logger.log(`Successfully deleted the course`); @@ -259,8 +260,7 @@ export class AdminController { try { this.logger.log(`Getting all transactions between admin and consumers.`); - const transactions = this.adminService.getTransactions(adminId); - + const transactions = await this.adminService.getTransactions(adminId); this.logger.log(`Successfully fetched all the transactions between admin and consumers`); res.status(HttpStatus.OK).json({ @@ -289,7 +289,7 @@ export class AdminController { const providerId = reqBody.providerId; const credits = reqBody.credits; - const provider = this.adminService.addOrRemoveCreditsToProvider(adminId, providerId, credits); + const provider = await this.adminService.addOrRemoveCreditsToProvider(adminId, providerId, credits); this.logger.log(`Succesfully Added credits to providers' wallet.`); @@ -319,7 +319,7 @@ export class AdminController { const providerId = reqBody.providerId; const credits = reqBody.credits; - const provider = this.adminService.addOrRemoveCreditsToProvider(adminId, providerId, credits); + const provider = await this.adminService.addOrRemoveCreditsToProvider(adminId, providerId, credits); this.logger.log(`Succesfully reduced credits from providers' wallet.`); diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts index 1663260..eed8ad6 100644 --- a/src/admin/admin.module.ts +++ b/src/admin/admin.module.ts @@ -3,10 +3,12 @@ import { AdminController } from './admin.controller'; import { AdminService } from './admin.service'; import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaModule } from 'nestjs-prisma'; +import { MockWalletModule } from 'src/mock-wallet/mock-wallet.module'; +import { MockWalletService } from 'src/mock-wallet/mock-wallet.service'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, MockWalletModule], controllers: [AdminController], - providers: [AdminService, PrismaService] + providers: [AdminService, PrismaService, MockWalletService] }) export class AdminModule {} diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index c0eecbe..c84fe0b 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -3,13 +3,13 @@ import { HttpException, Injectable, NotFoundException } from '@nestjs/common'; import axios, { AxiosResponse } from 'axios'; import { PrismaService } from '../prisma/prisma.service'; import { Provider, ProviderStatus, Course, CourseVerificationStatus } from '@prisma/client'; -import { omit } from 'lodash'; import { EditProvider } from './dto/edit-provider.dto'; +import { MockWalletService } from '../mock-wallet/mock-wallet.service'; @Injectable() export class AdminService { - constructor(private prisma: PrismaService) {} + constructor(private prisma: PrismaService, private wallet: MockWalletService) {} async verifyProvider(providerId: number) { let providerInfo = await this.prisma.provider.findUnique({ @@ -111,15 +111,16 @@ export class AdminService { }); } - async getTransactions(adminId: number): Promise { + async getTransactions(adminId: number) { - const walletService = process.env.MOCK_WALLET_SERVICE_URL; - const endpoint = `${adminId}/transactions/consumers`; - const url = walletService + endpoint; + // const walletService = process.env.WALLET_SERVICE_URL; + // const endpoint = `/admin/${adminId}/transactions/consumers`; + // const url = walletService + endpoint; try { - const response: AxiosResponse = await axios.get(url); - return response.data; + // const response: AxiosResponse = await axios.get(url); + const transactions = this.wallet.getTransactions(adminId); + return transactions.data; } catch (err) { throw new Error(`Failed to fetch data: ${err.message}`); @@ -127,20 +128,26 @@ export class AdminService { } async addOrRemoveCreditsToProvider(adminId: number, providerId: number, credits: number) { - const walletService = process.env.MOCK_WALLET_SERVICE_URL; - let endpoint: string; - if(credits >= 0) { - endpoint = `${adminId}/add-credits`; - } else { - endpoint = `${adminId}/reduce-credits`; - } - const url = walletService + endpoint; - const requestBody = { - consumerId: providerId, - credits: credits - }; + // const walletService = process.env.WALLET_SERVICE_URL; + // let endpoint: string; + // if(credits >= 0) { + // endpoint = `/admin/${adminId}/add-credits`; + // } else { + // endpoint = `/admin/${adminId}/reduce-credits`; + // } + // const url = walletService + endpoint; + // const requestBody = { + // consumerId: providerId, + // credits: credits + // }; try { - const response = await axios.post(url, requestBody); + // const response = await axios.post(url, requestBody); + let response; + if(credits >= 0) { + response = this.wallet.addCredits(adminId, providerId, credits); + } else { + response = this.wallet.reduceCredits(adminId, providerId, credits); + } return response; } catch (err) { throw new Error(`Failed to send POST request to walletService.`); diff --git a/src/admin/dto/edit-provider.dto.ts b/src/admin/dto/edit-provider.dto.ts index cbdec15..cc7b2f4 100644 --- a/src/admin/dto/edit-provider.dto.ts +++ b/src/admin/dto/edit-provider.dto.ts @@ -1,10 +1,5 @@ -import { ProviderStatus } from "@prisma/client"; +import { PartialType } from "@nestjs/swagger"; +import { ProviderProfileResponse } from "./provider-profile-response.dto"; -export class EditProvider { - readonly id: number; - readonly name: string; - readonly email: string; - readonly walletId: number; - readonly paymentInfo: any; - readonly status: ProviderStatus; +export class EditProvider extends PartialType(ProviderProfileResponse){ } \ No newline at end of file diff --git a/src/admin/dto/provider-profile-response.dto.ts b/src/admin/dto/provider-profile-response.dto.ts index 619a2b2..0f24713 100644 --- a/src/admin/dto/provider-profile-response.dto.ts +++ b/src/admin/dto/provider-profile-response.dto.ts @@ -1,11 +1,26 @@ -import { Course, ProviderStatus } from "@prisma/client"; +import { ApiProperty } from "@nestjs/swagger"; +import { ProviderStatus } from "@prisma/client"; export class ProviderProfileResponse { - readonly id: number; - readonly name: string; - readonly email: string; - readonly walletId: number; - readonly paymentInfo: any; - readonly status: ProviderStatus; + @ApiProperty({required: false}) + id: number; + + @ApiProperty() + name: string; + + @ApiProperty({format: 'email'}) + email: string; + + @ApiProperty() + password: string; + + @ApiProperty() + walletId: number; + + @ApiProperty() + paymentInfo: any; + + @ApiProperty() + status: ProviderStatus; // readonly courses: Course[]; } \ No newline at end of file diff --git a/src/app.module.ts b/src/app.module.ts index d3ac4c5..a9f1bff 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,6 +5,7 @@ import { AppService } from "./app.service"; import { PrismaModule } from "./prisma/prisma.module"; import { PrismaService } from "./prisma/prisma.service"; import { AdminModule } from './admin/admin.module'; +import { MockWalletModule } from './mock-wallet/mock-wallet.module'; @Module({ imports: [ @@ -13,6 +14,7 @@ import { AdminModule } from './admin/admin.module'; }), PrismaModule, AdminModule, + MockWalletModule, ], controllers: [AppController], providers: [AppService, PrismaService], diff --git a/src/mock-wallet/mock-wallet.controller.spec.ts b/src/mock-wallet/mock-wallet.controller.spec.ts new file mode 100644 index 0000000..3ccb1b7 --- /dev/null +++ b/src/mock-wallet/mock-wallet.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MockWalletController } from './mock-wallet.controller'; + +describe('MockWalletController', () => { + let controller: MockWalletController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [MockWalletController], + }).compile(); + + controller = module.get(MockWalletController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/mock-wallet/mock-wallet.controller.ts b/src/mock-wallet/mock-wallet.controller.ts new file mode 100644 index 0000000..9bea609 --- /dev/null +++ b/src/mock-wallet/mock-wallet.controller.ts @@ -0,0 +1,4 @@ +import { Controller } from '@nestjs/common'; + +@Controller('mock-wallet') +export class MockWalletController {} diff --git a/src/mock-wallet/mock-wallet.module.ts b/src/mock-wallet/mock-wallet.module.ts new file mode 100644 index 0000000..0e0ca25 --- /dev/null +++ b/src/mock-wallet/mock-wallet.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { MockWalletController } from './mock-wallet.controller'; +import { MockWalletService } from './mock-wallet.service'; + +@Module({ + controllers: [MockWalletController], + providers: [MockWalletService] +}) +export class MockWalletModule {} diff --git a/src/mock-wallet/mock-wallet.service.spec.ts b/src/mock-wallet/mock-wallet.service.spec.ts new file mode 100644 index 0000000..5871d3c --- /dev/null +++ b/src/mock-wallet/mock-wallet.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MockWalletService } from './mock-wallet.service'; + +describe('MockWalletService', () => { + let service: MockWalletService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [MockWalletService], + }).compile(); + + service = module.get(MockWalletService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/mock-wallet/mock-wallet.service.ts b/src/mock-wallet/mock-wallet.service.ts new file mode 100644 index 0000000..3f725f9 --- /dev/null +++ b/src/mock-wallet/mock-wallet.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class MockWalletService { + + getTransactions(adminId: number) { + return { + status: 200, + data: [ + { + transactionId: 1, + fromId: adminId, + toId: 2, + credits: 120, + type: 'creditRequest', + descripiton: `A credit request of 120 credits was made from consumer with id 2`, + createdAT: new Date() + }, + { + transactionId: 4, + fromId: adminId, + toId: 3, + credits: 100, + type: 'creditRequest', + descripiton: `A credit request of 100 credits was made from consumer with id 3`, + createdAT: new Date() + }, + ] + } + } + + addCredits(adminId: number, providerId: number, credits: number) { + return { + message: "Credits added successfully for the providers' wallet", + data: { + credits: credits + } + } + } + + reduceCredits(adminId: number, providerId: number, credits: number) { + return { + message: "Credits removed successfully from the providers' wallet", + data: { + credits: -credits + } + } + } +} From 703006e76e54b98eff04930bb65585efae4a276e Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Tue, 31 Oct 2023 11:39:09 +0530 Subject: [PATCH 08/88] Added sample data, constraints for dtos --- .../20231031053000_init/migration.sql | 88 +++++++++++++++++++ prisma/migrations/migration_lock.toml | 3 + prisma/seed.ts | 80 ++++++++++++++++- src/admin/admin.controller.ts | 10 +-- .../dto/provider-profile-response.dto.ts | 15 ++++ src/admin/dto/verify-course.dto.ts | 10 ++- 6 files changed, 197 insertions(+), 9 deletions(-) create mode 100644 prisma/migrations/20231031053000_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml diff --git a/prisma/migrations/20231031053000_init/migration.sql b/prisma/migrations/20231031053000_init/migration.sql new file mode 100644 index 0000000..e83f164 --- /dev/null +++ b/prisma/migrations/20231031053000_init/migration.sql @@ -0,0 +1,88 @@ +-- CreateEnum +CREATE TYPE "ProviderStatus" AS ENUM ('pending', 'verified', 'rejected'); + +-- CreateEnum +CREATE TYPE "CourseVerificationStatus" AS ENUM ('pending', 'accepted', 'rejected'); + +-- CreateEnum +CREATE TYPE "CourseStatus" AS ENUM ('active', 'inactive', 'archived'); + +-- CreateEnum +CREATE TYPE "CourseProgressStatus" AS ENUM ('inProgress', 'completed'); + +-- CreateEnum +CREATE TYPE "TransactionType" AS ENUM ('purchase', 'creditRequest', 'settlement'); + +-- CreateTable +CREATE TABLE "Admin" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "walletId" INTEGER NOT NULL, + + CONSTRAINT "Admin_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Provider" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "walletId" INTEGER NOT NULL, + "paymentInfo" JSONB, + "status" "ProviderStatus" NOT NULL DEFAULT 'pending', + + CONSTRAINT "Provider_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Course" ( + "id" SERIAL NOT NULL, + "providerId" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "courseLink" TEXT NOT NULL, + "imgLink" TEXT NOT NULL, + "credits" INTEGER NOT NULL, + "noOfLessons" INTEGER NOT NULL, + "language" TEXT[], + "duration" INTEGER NOT NULL, + "competency" JSONB NOT NULL, + "author" TEXT NOT NULL, + "avgRating" DOUBLE PRECISION, + "status" "CourseStatus" NOT NULL, + "availabilityTime" TIMESTAMP(3), + "verificationStatus" "CourseVerificationStatus" NOT NULL, + "cqfScore" INTEGER, + "impactScore" DOUBLE PRECISION, + + CONSTRAINT "Course_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "UserCourse" ( + "id" SERIAL NOT NULL, + "userId" TEXT NOT NULL, + "courseId" INTEGER NOT NULL, + "purchasedAt" TIMESTAMP(3) NOT NULL, + "status" "CourseProgressStatus" NOT NULL, + "courseCompletionScore" DOUBLE PRECISION, + "rating" INTEGER, + "feedback" TEXT, + + CONSTRAINT "UserCourse_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Admin_email_key" ON "Admin"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Provider_email_key" ON "Provider"("email"); + +-- AddForeignKey +ALTER TABLE "Course" ADD CONSTRAINT "Course_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserCourse" ADD CONSTRAINT "UserCourse_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/prisma/seed.ts b/prisma/seed.ts index 305805e..b3b1d96 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -30,6 +30,18 @@ async function main() { }, }); + const admin3 = await prisma.admin.upsert({ + where: { id: 3 }, + update: {}, + create: { + id: 3, + name: 'admin3', + email: "admin3@gmail.com", + password: "admin3", + walletId: 4 + }, + }); + const provider1 = await prisma.provider.upsert({ where: { id: 1 }, update: {}, @@ -70,6 +82,26 @@ async function main() { } }); + const provider3 = await prisma.provider.upsert({ + where: { id: 3 }, + update: {}, + create: { + id: 3, + name: "lern", + email: "lern@gmail.in", + password: "lern@999", + walletId: 6, + paymentInfo: { + bankAccNo: "1111111116", + otherDetails: { + + } + }, + status: 'rejected', + // courses: [] + } + }); + const course1 = await prisma.course.create({ data: { id: 1, @@ -95,10 +127,56 @@ async function main() { } }); + const course2 = await prisma.course.create({ + data: { + id: 2, + providerId: 1, + title: "Introduction to Programming", + description: "This course covers all the fundamentals of programming", + courseLink: "https://udemy.com/courses/jQKsLpm", + imgLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", + credits: 160, + noOfLessons: 100, + language: ["english", "hindi"], + duration: 50, + competency: { + 6: ["Level5", "Level4"], + 10: [ "Level1", "Level2" ] + }, + author: "James Franco", + status: CourseStatus.active, + availabilityTime: new Date("2023-08-10"), + verificationStatus: CourseVerificationStatus.pending, + } + }); + + const course3 = await prisma.course.create({ + data: { + id: 3, + providerId: 1, + title: "Introduction to Compiler Engineering", + description: "This course covers how compilers are built and also teaches you about how to create custom programming languages", + courseLink: "https://udemy.com/courses/jQKsLpm", + imgLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", + credits: 160, + noOfLessons: 100, + language: ["english", "hindi"], + duration: 50, + competency: { + 3: ["Level2", "Level3"], + 5: [ "Level4" ] + }, + author: "Ramakrishna Upadrasta", + status: CourseStatus.active, + availabilityTime: new Date("2023-10-10"), + verificationStatus: CourseVerificationStatus.rejected, + } + }); + const resp = await prisma.course.findMany({}); console.log("All courses: ", resp); - console.log({ admin1, admin2, provider1, provider2, course1 }); + console.log({ admin1, admin2, admin3, provider1, provider2, provider3, course1, course2, course3 }); } // execute the main function diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 6f6180f..304174f 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -83,7 +83,7 @@ export class AdminController { this.logger.log(`Getting provider information for id ${providerId}`); const updatedProviderInfo = { - id: providerDto.id, + id: providerId, name: providerDto.name, email: providerDto.email, password: providerDto.password, @@ -139,7 +139,7 @@ export class AdminController { @ApiOperation({ summary: "Get a course, given its courseId"}) @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) - @Get('/course/:courseId') + @Get('/courses/:courseId') async getCourseById ( @Param("courseId", ParseIntPipe) courseId: number, @Res() res ){ @@ -168,7 +168,7 @@ export class AdminController { @ApiOperation({ summary: "Accept course and assign a cqf_score"}) @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) - @Patch('/course/:courseId/accept') + @Patch('/courses/:courseId/accept') async acceptCourse ( @Param("courseId", ParseIntPipe) courseId: number, @Body() verifyBody: CourseVerify, @Res() res ) { @@ -196,7 +196,7 @@ export class AdminController { @ApiOperation({ summary: "Reject a course given its courseId"}) @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) - @Patch('/course/:courseId/reject') + @Patch('/courses/:courseId/reject') async rejectCourse ( @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { @@ -224,7 +224,7 @@ export class AdminController { @ApiOperation({ summary: "Remove a course given its courseId"}) @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) - @Delete('/course/:courseId') + @Delete('/courses/:courseId') async removeCourse ( @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { diff --git a/src/admin/dto/provider-profile-response.dto.ts b/src/admin/dto/provider-profile-response.dto.ts index 0f24713..59d1c24 100644 --- a/src/admin/dto/provider-profile-response.dto.ts +++ b/src/admin/dto/provider-profile-response.dto.ts @@ -1,26 +1,41 @@ import { ApiProperty } from "@nestjs/swagger"; import { ProviderStatus } from "@prisma/client"; +import { IsEmail, IsEnum, IsJSON, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; + export class ProviderProfileResponse { @ApiProperty({required: false}) + @IsNumber() + @IsOptional() id: number; @ApiProperty() + @IsOptional() + @IsString() name: string; @ApiProperty({format: 'email'}) + @IsEmail() + @IsOptional() email: string; @ApiProperty() + @IsString() + @IsOptional() password: string; @ApiProperty() + @IsNumber() + @IsOptional() walletId: number; @ApiProperty() + @IsOptional() paymentInfo: any; @ApiProperty() + @IsOptional() + @IsEnum(ProviderStatus) status: ProviderStatus; // readonly courses: Course[]; } \ No newline at end of file diff --git a/src/admin/dto/verify-course.dto.ts b/src/admin/dto/verify-course.dto.ts index 5836367..7652b5d 100644 --- a/src/admin/dto/verify-course.dto.ts +++ b/src/admin/dto/verify-course.dto.ts @@ -1,6 +1,10 @@ -import { CourseVerificationStatus } from "@prisma/client"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsNumber } from 'class-validator'; + export class CourseVerify { - readonly cqf_score: number; - readonly verificationStatus: CourseVerificationStatus; + @ApiProperty() + @IsNumber() + @IsNotEmpty() + cqf_score: number; } \ No newline at end of file From 245bc5c3233e925e629c50f2b4f4956582007a73 Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Tue, 31 Oct 2023 11:40:38 +0530 Subject: [PATCH 09/88] Removed migrations, node_modules --- .../20231031053000_init/migration.sql | 88 ------------------- prisma/migrations/migration_lock.toml | 3 - 2 files changed, 91 deletions(-) delete mode 100644 prisma/migrations/20231031053000_init/migration.sql delete mode 100644 prisma/migrations/migration_lock.toml diff --git a/prisma/migrations/20231031053000_init/migration.sql b/prisma/migrations/20231031053000_init/migration.sql deleted file mode 100644 index e83f164..0000000 --- a/prisma/migrations/20231031053000_init/migration.sql +++ /dev/null @@ -1,88 +0,0 @@ --- CreateEnum -CREATE TYPE "ProviderStatus" AS ENUM ('pending', 'verified', 'rejected'); - --- CreateEnum -CREATE TYPE "CourseVerificationStatus" AS ENUM ('pending', 'accepted', 'rejected'); - --- CreateEnum -CREATE TYPE "CourseStatus" AS ENUM ('active', 'inactive', 'archived'); - --- CreateEnum -CREATE TYPE "CourseProgressStatus" AS ENUM ('inProgress', 'completed'); - --- CreateEnum -CREATE TYPE "TransactionType" AS ENUM ('purchase', 'creditRequest', 'settlement'); - --- CreateTable -CREATE TABLE "Admin" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "email" TEXT NOT NULL, - "password" TEXT NOT NULL, - "walletId" INTEGER NOT NULL, - - CONSTRAINT "Admin_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Provider" ( - "id" SERIAL NOT NULL, - "name" TEXT NOT NULL, - "email" TEXT NOT NULL, - "password" TEXT NOT NULL, - "walletId" INTEGER NOT NULL, - "paymentInfo" JSONB, - "status" "ProviderStatus" NOT NULL DEFAULT 'pending', - - CONSTRAINT "Provider_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Course" ( - "id" SERIAL NOT NULL, - "providerId" INTEGER NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT NOT NULL, - "courseLink" TEXT NOT NULL, - "imgLink" TEXT NOT NULL, - "credits" INTEGER NOT NULL, - "noOfLessons" INTEGER NOT NULL, - "language" TEXT[], - "duration" INTEGER NOT NULL, - "competency" JSONB NOT NULL, - "author" TEXT NOT NULL, - "avgRating" DOUBLE PRECISION, - "status" "CourseStatus" NOT NULL, - "availabilityTime" TIMESTAMP(3), - "verificationStatus" "CourseVerificationStatus" NOT NULL, - "cqfScore" INTEGER, - "impactScore" DOUBLE PRECISION, - - CONSTRAINT "Course_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "UserCourse" ( - "id" SERIAL NOT NULL, - "userId" TEXT NOT NULL, - "courseId" INTEGER NOT NULL, - "purchasedAt" TIMESTAMP(3) NOT NULL, - "status" "CourseProgressStatus" NOT NULL, - "courseCompletionScore" DOUBLE PRECISION, - "rating" INTEGER, - "feedback" TEXT, - - CONSTRAINT "UserCourse_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Admin_email_key" ON "Admin"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "Provider_email_key" ON "Provider"("email"); - --- AddForeignKey -ALTER TABLE "Course" ADD CONSTRAINT "Course_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "UserCourse" ADD CONSTRAINT "UserCourse_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92..0000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file From 56e2b62f93c820080f94641491e61558c776c0aa Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Tue, 31 Oct 2023 11:55:53 +0530 Subject: [PATCH 10/88] seed data --- .gitignore | 5 ++- prisma/seed.ts | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 prisma/seed.ts diff --git a/.gitignore b/.gitignore index f6a091e..8910bfb 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,7 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +# prisma +prisma/migrations \ No newline at end of file diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..d16b6cd --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,111 @@ +import { PrismaClient } from '@prisma/client' +const prisma = new PrismaClient() +async function main() { + + const response = await prisma.provider.create({ + data: { + name: "provider 1", + email: "provider1@xyz.com", + password: "abc", + wallet: { + create: { + type: 'provider', + } + } + } + }) + + const response1 = await prisma.course.createMany({ + data: [{ + providerId: response.id, + title: "course1", + description: "course1", + courseLink: "abc1", + imgLink: "qwe1", + credits: 4, + noOfLessons: 3, + language: "en", + duration: 4, + competency: [], + author: "abcd1", + status: "active", + availabilityTime: new Date("2024-05-01").toISOString() + }, { + providerId: response.id, + title: "course2", + description: "course2", + courseLink: "abc2", + imgLink: "qwe2", + credits: 5, + noOfLessons: 3, + language: "en", + duration: 4, + competency: [], + author: "abcd2", + status: "active", + availabilityTime: new Date("2024-05-01").toISOString() + }, { + providerId: response.id, + title: "course3", + description: "course3", + courseLink: "abc3", + imgLink: "qwe3", + credits: 2, + noOfLessons: 3, + language: "en", + duration: 4, + competency: [], + author: "abcd3", + status: "active", + availabilityTime: new Date("2024-05-01").toISOString() + }, { + providerId: response.id, + title: "course4", + description: "course4", + courseLink: "abc4", + imgLink: "qwe4", + credits: 4, + noOfLessons: 3, + language: "en", + duration: 4, + competency: [], + author: "abcd4", + status: "active", + availabilityTime: new Date("2024-05-01").toISOString() + }] + }) + + const response3 = await prisma.userCourse.createMany({ + data: [{ + userId: "xyz1", + feedback: "sample feedback 1", + rating: 2, + courseId: 1 + }, { + userId: "xyz2", + feedback: "sample feedback 2", + rating: 4, + courseId: 2 + }, { + userId: "xyz3", + feedback: "sample feedback 3", + rating: 3, + courseId: 2 + }, { + userId: "xyz4", + feedback: "sample feedback 4", + rating: 3, + courseId: 3 + }] + }) + console.log(response) +} +main() + .then(async () => { + await prisma.$disconnect() + }) + .catch(async (e) => { + console.error(e) + await prisma.$disconnect() + process.exit(1) + }) \ No newline at end of file From 9d209aacaed4088d68c41bd8dfb20de39cbd8339 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Tue, 31 Oct 2023 16:56:02 +0530 Subject: [PATCH 11/88] fixes and refactoring --- prisma/schema.prisma | 4 ++-- prisma/seed.ts | 8 ++++---- src/course/course.service.ts | 8 ++++---- src/course/dto/add-course.dto.ts | 6 +++--- src/course/dto/completion.dto.ts | 18 ++++++++++++++++++ src/course/dto/course.dto.ts | 2 +- src/provider/provider.controller.ts | 6 +++--- src/provider/provider.service.ts | 9 +++++---- 8 files changed, 40 insertions(+), 21 deletions(-) create mode 100644 src/course/dto/completion.dto.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ce99b9c..e9fda31 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -87,7 +87,7 @@ model Course { imgLink String credits Int noOfLessons Int? - language String + language String[] duration Int competency Json[] author String @@ -110,7 +110,7 @@ model UserCourse { courseCompletionScore Float? rating Int? feedback String? - course Course @relation(fields: [courseId], references: [id]) + course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) @@unique([userId, courseId]) } diff --git a/prisma/seed.ts b/prisma/seed.ts index d16b6cd..d6b9289 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -24,7 +24,7 @@ async function main() { imgLink: "qwe1", credits: 4, noOfLessons: 3, - language: "en", + language: ["en"], duration: 4, competency: [], author: "abcd1", @@ -38,7 +38,7 @@ async function main() { imgLink: "qwe2", credits: 5, noOfLessons: 3, - language: "en", + language: ["en"], duration: 4, competency: [], author: "abcd2", @@ -52,7 +52,7 @@ async function main() { imgLink: "qwe3", credits: 2, noOfLessons: 3, - language: "en", + language: ["en"], duration: 4, competency: [], author: "abcd3", @@ -66,7 +66,7 @@ async function main() { imgLink: "qwe4", credits: 4, noOfLessons: 3, - language: "en", + language: ["en"], duration: 4, competency: [], author: "abcd4", diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 8984794..b98ec74 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -2,6 +2,7 @@ import { Injectable } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { AddCourseDto } from "./dto/add-course.dto"; import { CourseProgressStatus } from "@prisma/client"; +import { CompleteCourseDto } from "./dto/completion.dto"; @Injectable() export class CourseService { @@ -28,7 +29,7 @@ export class CourseService { async deleteCourse(courseId: number) { - this.prisma.course.delete({ + await this.prisma.course.delete({ where: { id: courseId } @@ -53,13 +54,12 @@ export class CourseService { }) } - async markCourseComplete(courseId: number, userId: string) { + async markCourseComplete(completeCourseDto: CompleteCourseDto) { await this.prisma.userCourse.update({ where: { userId_courseId: { - courseId, - userId + ...completeCourseDto } }, data: { diff --git a/src/course/dto/add-course.dto.ts b/src/course/dto/add-course.dto.ts index c60303a..bd0df5c 100644 --- a/src/course/dto/add-course.dto.ts +++ b/src/course/dto/add-course.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { CourseStatus, CourseVerificationStatus } from "@prisma/client"; -import { IsDate, IsInt, IsJSON, IsNotEmpty, IsOptional, IsString, Min } from "class-validator"; +import { IsArray, IsDate, IsInt, IsJSON, IsNotEmpty, IsOptional, IsString, Min } from "class-validator"; export class AddCourseDto { @@ -44,8 +44,8 @@ export class AddCourseDto { // language @ApiProperty() @IsNotEmpty() - @IsString() - language: string; + @IsArray() + language: string[]; // course duration @ApiProperty() diff --git a/src/course/dto/completion.dto.ts b/src/course/dto/completion.dto.ts new file mode 100644 index 0000000..86dee32 --- /dev/null +++ b/src/course/dto/completion.dto.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsInt, IsNotEmpty, IsString } from "class-validator"; + + +export class CompleteCourseDto { + + // course ID + @ApiProperty() + @IsNotEmpty() + @IsInt() + courseId: number; + + // user ID + @ApiProperty() + @IsNotEmpty() + @IsString() + userId: string; +} \ No newline at end of file diff --git a/src/course/dto/course.dto.ts b/src/course/dto/course.dto.ts index 89ab9b7..5ea5032 100644 --- a/src/course/dto/course.dto.ts +++ b/src/course/dto/course.dto.ts @@ -10,7 +10,7 @@ export class CourseResponseDto { readonly imgLink: string; readonly credits: number; readonly noOfLessons: number | null; - readonly language: string; + readonly language: string[]; readonly duration: number; readonly competency: Prisma.JsonValue[]; readonly author: string; diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index 5fec65a..ffd510a 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -9,6 +9,7 @@ import { ViewProfileResponseDto } from './dto/view-profile.dto'; import { FeedbackResponseDto } from './dto/feedback.dto'; import { PurchaseResponseDto } from './dto/purchase.dto'; import { CourseResponseDto } from 'src/course/dto/course.dto'; +import { CompleteCourseDto } from 'src/course/dto/completion.dto'; @Controller('provider') export class ProviderController { @@ -173,11 +174,10 @@ export class ProviderController { // Mark course as complete for a user async markCourseComplete( @Param("providerId", ParseIntPipe) providerId: number, - @Param("courseId", ParseIntPipe) courseId: number, - @Param("userId") userId: string, + @Body() completeCourseDto: CompleteCourseDto, @Res() res ) { - await this.providerService.markCourseComplete(providerId, courseId, userId); + await this.providerService.markCourseComplete(providerId, completeCourseDto); res.status(HttpStatus.OK).json({ message: "course marked complete", diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index b2adedc..14892d1 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -9,6 +9,7 @@ import { CourseService } from 'src/course/course.service'; import { Feedback, FeedbackResponseDto } from './dto/feedback.dto'; import { PurchaseResponseDto } from './dto/purchase.dto'; import { CourseResponseDto } from 'src/course/dto/course.dto'; +import { CompleteCourseDto } from 'src/course/dto/completion.dto'; @Injectable() export class ProviderService { @@ -91,7 +92,7 @@ export class ProviderService { if(course.providerId != providerId) throw new BadRequestException("Course does not belong to this provider"); - return this.courseService.deleteCourse(courseId); + await this.courseService.deleteCourse(courseId); } async getCourses(providerId: number): Promise { @@ -146,9 +147,9 @@ export class ProviderService { } - async markCourseComplete(providerId: number, courseId: number, userId: string) { + async markCourseComplete(providerId: number, completeCourseDto: CompleteCourseDto) { - const course = await this.courseService.getCourse(courseId); + const course = await this.courseService.getCourse(completeCourseDto.courseId); if(!course) throw new NotFoundException("Course does not exist"); @@ -156,7 +157,7 @@ export class ProviderService { throw new BadRequestException("Course does not belong to this provider"); try { - await this.courseService.markCourseComplete(courseId, userId); + await this.courseService.markCourseComplete(completeCourseDto); } catch { throw new NotFoundException("This user has not subscribed to this course"); } From 6cf9c15eaa401eb0a49806d9109464ee0647d7a4 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Tue, 31 Oct 2023 17:20:52 +0530 Subject: [PATCH 12/88] isJson removed --- prisma/seed.ts | 66 +++++++++++++------------- src/course/dto/add-course.dto.ts | 1 - src/provider/dto/signup.dto.ts | 1 - src/provider/dto/update-profile.dto.ts | 1 - 4 files changed, 33 insertions(+), 36 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index d6b9289..850c0c8 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -4,9 +4,9 @@ async function main() { const response = await prisma.provider.create({ data: { - name: "provider 1", - email: "provider1@xyz.com", - password: "abc", + name: "Vijay Salgaonkar", + email: "vijaysalgaonkar@gmail.com", + password: "9d209aacaed4088d68c41bd8dfb20de39cbd8339", wallet: { create: { type: 'provider', @@ -18,58 +18,58 @@ async function main() { const response1 = await prisma.course.createMany({ data: [{ providerId: response.id, - title: "course1", - description: "course1", - courseLink: "abc1", - imgLink: "qwe1", + title: "NestJS Complete", + description: "Build full featured backend APIs incredibly quickly with Nest, TypeORM, and Typescript. Includes testing and deployment!", + courseLink: "https://www.udemy.com/course/nestjs-the-complete-developers-guide/", + imgLink: "https://courses.nestjs.com/img/logo.svg", credits: 4, noOfLessons: 3, language: ["en"], duration: 4, competency: [], - author: "abcd1", + author: "Stephen Grider", status: "active", availabilityTime: new Date("2024-05-01").toISOString() }, { providerId: response.id, - title: "course2", - description: "course2", - courseLink: "abc2", - imgLink: "qwe2", + title: "Graphic Design Masterclass", + description: "The Ultimate Graphic Design Course Which Covers Photoshop, Illustrator, InDesign, Design Theory, Branding & Logo Design", + courseLink: "https://www.udemy.com/course/graphic-design-masterclass-everything-you-need-to-know/", + imgLink: "https://www.unite.ai/wp-content/uploads/2023/05/emily-bernal-v9vII5gV8Lw-unsplash.jpg", credits: 5, noOfLessons: 3, language: ["en"], duration: 4, competency: [], - author: "abcd2", + author: "Lindsay Marsh", status: "active", availabilityTime: new Date("2024-05-01").toISOString() }, { providerId: response.id, - title: "course3", - description: "course3", - courseLink: "abc3", - imgLink: "qwe3", + title: "Python for Data Science", + description: "Learn how to use NumPy, Pandas, Seaborn , Matplotlib , Plotly , Scikit-Learn , Machine Learning, Tensorflow , and more", + courseLink: "https://www.udemy.com/course/python-for-data-science-and-machine-learning-bootcamp/", + imgLink: "https://blog.imarticus.org/wp-content/uploads/2021/12/learn-Python-for-data-science.jpg", credits: 2, noOfLessons: 3, language: ["en"], duration: 4, competency: [], - author: "abcd3", + author: "Jose Portilla", status: "active", availabilityTime: new Date("2024-05-01").toISOString() }, { providerId: response.id, - title: "course4", - description: "course4", - courseLink: "abc4", - imgLink: "qwe4", + title: "Microsoft Excel", + description: "Excel with this A-Z Microsoft Excel Course. Microsoft Excel 2010, 2013, 2016, Excel 2019 and Microsoft/Office 365/2023", + courseLink: "https://www.udemy.com/course/microsoft-excel-2013-from-beginner-to-advanced-and-beyond/", + imgLink: "https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Microsoft_Excel_2013-2019_logo.svg/587px-Microsoft_Excel_2013-2019_logo.svg.png", credits: 4, noOfLessons: 3, language: ["en"], duration: 4, competency: [], - author: "abcd4", + author: "Kyle Pew", status: "active", availabilityTime: new Date("2024-05-01").toISOString() }] @@ -77,24 +77,24 @@ async function main() { const response3 = await prisma.userCourse.createMany({ data: [{ - userId: "xyz1", - feedback: "sample feedback 1", - rating: 2, + userId: "56e2b", + feedback: "Great course", + rating: 4, courseId: 1 }, { - userId: "xyz2", - feedback: "sample feedback 2", + userId: "c8200", + feedback: "Instructor is very friendly", rating: 4, courseId: 2 }, { - userId: "xyz3", - feedback: "sample feedback 3", + userId: "f9464", + feedback: "Some more real world applications could be discussed", rating: 3, courseId: 2 }, { - userId: "xyz4", - feedback: "sample feedback 4", - rating: 3, + userId: "91e61", + feedback: "Not satisfied with the content", + rating: 2, courseId: 3 }] }) diff --git a/src/course/dto/add-course.dto.ts b/src/course/dto/add-course.dto.ts index bd0df5c..98a5c77 100644 --- a/src/course/dto/add-course.dto.ts +++ b/src/course/dto/add-course.dto.ts @@ -57,7 +57,6 @@ export class AddCourseDto { // competency @ApiProperty() @IsNotEmpty() - // @IsJSON() competency: any[]; // author diff --git a/src/provider/dto/signup.dto.ts b/src/provider/dto/signup.dto.ts index 4d6ab40..cc26522 100644 --- a/src/provider/dto/signup.dto.ts +++ b/src/provider/dto/signup.dto.ts @@ -24,7 +24,6 @@ export class SignupDto { // payment info @ApiProperty() @IsOptional() - @IsJSON() paymentInfo?: any } diff --git a/src/provider/dto/update-profile.dto.ts b/src/provider/dto/update-profile.dto.ts index 60b3d41..ca7f373 100644 --- a/src/provider/dto/update-profile.dto.ts +++ b/src/provider/dto/update-profile.dto.ts @@ -23,7 +23,6 @@ export class UpdateProfileDto { // payment info @ApiProperty() - @IsJSON() @IsOptional() paymentInfo?: any } From ef5a9bfecf0c43848396a6315cef88a127982ed3 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Wed, 1 Nov 2023 14:01:16 +0530 Subject: [PATCH 13/88] provider status validation --- src/provider/provider.controller.ts | 4 ++-- src/provider/provider.service.ts | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index ffd510a..dc2c6be 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -64,7 +64,7 @@ export class ProviderController { const provider = await this.providerService.getProvider(providerId); res.status(HttpStatus.OK).json({ - message: "fetched successful", + message: "fetch successful", data : provider }) } @@ -170,7 +170,7 @@ export class ProviderController { @ApiOperation({ summary: 'Mark course as complete' }) @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:providerId/course/:courseId/completion/:userId") + @Patch("/:providerId/course/completion") // Mark course as complete for a user async markCourseComplete( @Param("providerId", ParseIntPipe) providerId: number, diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 14892d1..8839869 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -1,7 +1,7 @@ -import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { SignupDto } from './dto/signup.dto'; import { PrismaService } from 'src/prisma/prisma.service'; -import { WalletType } from '@prisma/client'; +import { ProviderStatus, WalletType } from '@prisma/client'; import { LoginDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { AddCourseDto } from 'src/course/dto/add-course.dto'; @@ -55,11 +55,11 @@ export class ProviderService { where: { id: providerId } - }) + }); if(!provider) throw new NotFoundException("provider does not exist"); - return provider + return provider; } async updateProfileInfo(providerId: number, updateProfileDto: UpdateProfileDto) { @@ -78,7 +78,10 @@ export class ProviderService { async addNewCourse(providerId: number, addCourseDto: AddCourseDto) { - await this.getProvider(providerId); + const provider = await this.getProvider(providerId); + + if(provider.status != ProviderStatus.verified) + throw new UnauthorizedException("Provider account is not verified"); return this.courseService.addCourse(providerId, addCourseDto); } From d6cb52bf212220feac9ad221b4d1846f9c48c951 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Thu, 2 Nov 2023 12:20:31 +0530 Subject: [PATCH 14/88] json fix --- src/course/dto/add-course.dto.ts | 2 +- src/provider/provider.service.ts | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/course/dto/add-course.dto.ts b/src/course/dto/add-course.dto.ts index 98a5c77..6786db8 100644 --- a/src/course/dto/add-course.dto.ts +++ b/src/course/dto/add-course.dto.ts @@ -57,7 +57,7 @@ export class AddCourseDto { // competency @ApiProperty() @IsNotEmpty() - competency: any[]; + competency: any; // author @ApiProperty() diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 8839869..4017653 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { SignupDto } from './dto/signup.dto'; import { PrismaService } from 'src/prisma/prisma.service'; -import { ProviderStatus, WalletType } from '@prisma/client'; +import { Prisma, ProviderStatus, WalletType } from '@prisma/client'; import { LoginDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { AddCourseDto } from 'src/course/dto/add-course.dto'; @@ -20,7 +20,15 @@ export class ProviderService { async createNewAccount(signupDto: SignupDto) { - const provider = await this.prisma.provider.create({ + let provider = await this.prisma.provider.findUnique({ + where : { + email: signupDto.email + } + }) + if(provider) + throw new BadRequestException("Account with that email ID already exists"); + + provider = await this.prisma.provider.create({ data: { ...signupDto, wallet: { From 7d1bc2cbc27b216cc5a26fc2907a7f51c9352c80 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Thu, 2 Nov 2023 20:00:05 +0530 Subject: [PATCH 15/88] search changes --- src/course/course.controller.ts | 7 +++---- src/course/course.service.ts | 14 +++++++------- src/course/dto/course.dto.ts | 4 ++-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index 6179198..a5431a7 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, Get, HttpStatus, Param, ParseIntPipe, Patch, Post, Res } from "@nestjs/common"; import { ApiOperation, ApiResponse } from "@nestjs/swagger"; import { CourseService } from "./course.service"; -import { SearchDto } from "./dto/search.dto"; import { CourseDto } from "./dto/course.dto"; import { FeedbackDto } from "./dto/feedback.dto"; @@ -11,13 +10,13 @@ export class CourseController { @ApiOperation({ summary: 'Search courses' }) @ApiResponse({ status: HttpStatus.OK, type: [CourseDto] }) - @Post("/search") + @Get("/search") // search courses async searchCourses( - @Body() searchDto: SearchDto, + // @Body() searchDto: SearchDto, @Res() res ) { - const courses = await this.courseService.searchCourses(searchDto); + const courses = await this.courseService.searchCourses(); res.status(HttpStatus.OK).json({ message: "search successful", diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 8d1f4cb..f8b67d7 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -10,15 +10,15 @@ export class CourseService { private prisma: PrismaService, ) {} - async searchCourses(searchDto: SearchDto): Promise { + async searchCourses(): Promise { const courses = await this.prisma.course.findMany({ - where: { - title: { - contains: searchDto.searchInput, - mode: "insensitive", - } - } + // where: { + // title: { + // contains: searchDto.searchInput, + // mode: "insensitive", + // } + // } }) diff --git a/src/course/dto/course.dto.ts b/src/course/dto/course.dto.ts index 123f24b..b2e8b15 100644 --- a/src/course/dto/course.dto.ts +++ b/src/course/dto/course.dto.ts @@ -10,9 +10,9 @@ export class CourseDto { readonly imgLink: string; readonly credits: number; readonly noOfLessons: number | null; - readonly language: string; + readonly language: string[]; readonly duration: number; - readonly competency: Prisma.JsonValue[]; + readonly competency: any; readonly author: string; readonly avgRating: number | null; readonly status: $Enums.CourseStatus; From 1e750f230f463cd2091462593da63388f5fb8d81 Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Mon, 6 Nov 2023 00:57:00 +0530 Subject: [PATCH 16/88] Resolve merge conflicts --- prisma/seed.ts | 40 +++++++++----- src/admin/admin.controller.ts | 1 - src/course/course.service.ts | 59 +++++++++++++++++++-- src/course/dto/edit-course.dto.ts | 81 +++++++++++++++++++++++++++++ src/provider/provider.controller.ts | 34 ++++++++++++ src/provider/provider.service.ts | 10 ++++ 6 files changed, 207 insertions(+), 18 deletions(-) create mode 100644 src/course/dto/edit-course.dto.ts diff --git a/prisma/seed.ts b/prisma/seed.ts index eb5de0e..6993921 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,4 +1,4 @@ -import { CourseStatus, CourseVerificationStatus, PrismaClient } from '@prisma/client' +import { CourseStatus, CourseVerificationStatus, PrismaClient, ProviderStatus } from '@prisma/client' const prisma = new PrismaClient() async function main() { @@ -12,7 +12,8 @@ async function main() { create: { type: 'provider', } - } + }, + status: ProviderStatus.verified } }); @@ -90,7 +91,11 @@ async function main() { noOfLessons: 3, language: ["en"], duration: 4, - competency: [], + competency: { + "API Development": ["Level1", "Level2"], + "Typescript": ["Level1"], + "Backend engineering": ["Level1"] + }, author: "Stephen Grider", status: "active", availabilityTime: new Date("2024-05-01").toISOString() @@ -104,7 +109,10 @@ async function main() { noOfLessons: 3, language: ["en"], duration: 4, - competency: [], + competency: { + "Photoshop": ["Level2", "Level3"], + "Understanding brand": ["Level1"] + }, author: "Lindsay Marsh", status: "active", availabilityTime: new Date("2024-05-01").toISOString() @@ -118,7 +126,11 @@ async function main() { noOfLessons: 3, language: ["en"], duration: 4, - competency: [], + competency: { + "Statistics": ["Level1"], + "Machine Learning": ["Level1", "Level2", "Level3"], + "MySQL": ["Level1"] + }, author: "Jose Portilla", status: "active", availabilityTime: new Date("2024-05-01").toISOString() @@ -132,7 +144,9 @@ async function main() { noOfLessons: 3, language: ["en"], duration: 4, - competency: [], + competency: { + "Excel": ["Level1", "Level2", "Level3", "Level4"] + }, author: "Kyle Pew", status: "active", availabilityTime: new Date("2024-05-01").toISOString() @@ -189,9 +203,9 @@ async function main() { language: ["english", "hindi"], duration: 48, competency: { - 9: ["Level1", "Level3"], - 11: ["Level1"], - 14: [ "Level5" ] + "Docker": ["Level1", "Level3"], + "Kubernetes": ["Level1"], + "Orchestration": [ "Level5" ] }, author: "Jason Frig", status: CourseStatus.active, @@ -213,8 +227,8 @@ async function main() { language: ["english", "hindi"], duration: 50, competency: { - 6: ["Level5", "Level4"], - 10: [ "Level1", "Level2" ] + "Logical Thinking": ["Level5", "Level4"], + "Python": [ "Level1", "Level2" ] }, author: "James Franco", status: CourseStatus.active, @@ -235,8 +249,8 @@ async function main() { language: ["english", "hindi"], duration: 50, competency: { - 3: ["Level2", "Level3"], - 5: [ "Level4" ] + "Compiler Design": ["Level2", "Level3"], + "LLVM": [ "Level4" ] }, author: "Ramakrishna Upadrasta", status: CourseStatus.active, diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 8481f73..1ea008a 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -87,7 +87,6 @@ export class AdminController { name: providerDto.name, email: providerDto.email, password: providerDto.password, - paymentInfo: providerDto.paymentInfo, walletId: providerDto.walletId, status: providerDto.status } diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 1c06c2d..3a73acd 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -1,10 +1,11 @@ -import { Injectable, NotFoundException } from "@nestjs/common"; +import { NotFoundException, BadRequestException, Injectable } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { FeedbackDto } from "./dto/feedback.dto"; import { CourseDto } from "./dto/course.dto"; import { AddCourseDto } from "./dto/add-course.dto"; +import { CourseProgressStatus, CourseStatus, CourseVerificationStatus } from "@prisma/client"; import { CompleteCourseDto } from "./dto/completion.dto"; -import { CourseProgressStatus } from "@prisma/client"; +import { EditCourseDto } from "./dto/edit-course.dto"; @Injectable() export class CourseService { @@ -26,14 +27,63 @@ export class CourseService { return courses; } - + async addCourse(providerId: number, addCourseDto: AddCourseDto) { - return this.prisma.course.create({ + return await this.prisma.course.create({ data: { providerId, ...addCourseDto } + }); + } + + async addPurchaseRecord(userId: string, courseId: number) { + + const record = this.prisma.userCourse.findFirst({ + where: { userId: userId, courseId: courseId } + }); + + if(record != null) { + throw new BadRequestException("Course already purchased by the user"); + } + + return await this.prisma.userCourse.create({ + data: { + userId, + courseId, + } + }); + } + + async archiveCourse(providerId: number, courseId: number) { + + return this.prisma.course.update({ + where: { id: courseId }, + data: { status: CourseStatus.archived } + }); + + } + + async editCourse(providerId: number, courseId: number, editCourseDto: EditCourseDto) { + + return await this.prisma.course.update({ + where: { id: courseId }, + data: { + providerId, + title: editCourseDto.title, + description: editCourseDto.description, + courseLink: editCourseDto.courseLink, + imgLink: editCourseDto.imgLink, + credits: editCourseDto.credits, + noOfLessons: editCourseDto.noOfLessons, + language: editCourseDto.language, + duration: editCourseDto.duration, + competency: editCourseDto.competency, + author: editCourseDto.author, + verificationStatus: CourseVerificationStatus.pending, + availabilityTime: editCourseDto.availabilityTime + } }) } @@ -99,6 +149,7 @@ export class CourseService { providerId } }) + } async getUserCourses(courseId: number) { diff --git a/src/course/dto/edit-course.dto.ts b/src/course/dto/edit-course.dto.ts new file mode 100644 index 0000000..af01794 --- /dev/null +++ b/src/course/dto/edit-course.dto.ts @@ -0,0 +1,81 @@ +import { ApiProperty, PartialType } from "@nestjs/swagger"; +import { AddCourseDto } from "./add-course.dto"; +import { IsArray, IsDate, IsInt, IsNotEmpty, IsOptional, IsString, Min } from "class-validator"; +import { CourseStatus } from "@prisma/client"; + +export class EditCourseDto { + + // course title + @ApiProperty() + @IsString() + @IsOptional() + title: string; + + // description + @ApiProperty() + @IsString() + @IsOptional() + description: string; + + // link for the course content + @ApiProperty() + @IsString() + @IsOptional() + courseLink: string; + + // course image + @ApiProperty() + @IsString() + @IsOptional() + imgLink: string; + + // number of credits required to purchase course + @ApiProperty() + @Min(0) + @IsInt() + @IsOptional() + credits: number; + + // Number of lessons + @ApiProperty() + @IsInt() + @IsOptional() + noOfLessons?: number; + + // language + @ApiProperty() + @IsArray() + @IsOptional() + language: string[]; + + // course duration + @ApiProperty() + @Min(0) + @IsInt() + @IsOptional() + duration: number; + + // competency + @ApiProperty() + @IsOptional() + competency: any; + + // author + @ApiProperty() + @IsString() + @IsOptional() + author: string; + + // course status (active/inactive/archived) + // @ApiProperty() + // @IsString() + // @IsOptional() + // status: CourseStatus; + + // course availability time + @ApiProperty() + @IsDate() + @IsOptional() + availabilityTime?: Date; + +} \ No newline at end of file diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index edfabda..3d0c092 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -10,6 +10,7 @@ import { FeedbackResponseDto } from './dto/feedback.dto'; import { PurchaseResponseDto } from './dto/purchase.dto'; import { CourseDto } from 'src/course/dto/course.dto'; import { CompleteCourseDto } from 'src/course/dto/completion.dto'; +import { EditCourseDto } from 'src/course/dto/edit-course.dto'; @Controller('provider') export class ProviderController { @@ -84,6 +85,39 @@ export class ProviderController { message: "account updated successfully", }) } + + @ApiOperation({ summary: 'edit course information' }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:providerId/course/:courseId") + // edit course information + async editCourse( + @Param("providerId", ParseIntPipe) providerId: number, + @Param("courseId", ParseIntPipe) courseId: number, + @Body() editCourseDto: EditCourseDto, + @Res() res + ) { + await this.providerService.editCourse(providerId, courseId, editCourseDto); + + res.status(HttpStatus.OK).json({ + message: "course edited successfully", + }) + } + + @ApiOperation({ summary: 'Archive course ' }) + @ApiResponse({ status: HttpStatus.OK }) + @Get("/:providerId/course/:courseId/archive") + // edit course information + async archiveCourse( + @Param("providerId", ParseIntPipe) providerId: number, + @Param("courseId", ParseIntPipe) courseId: number, + @Res() res + ) { + await this.providerService.archiveCourse(providerId, courseId); + + res.status(HttpStatus.OK).json({ + message: "course edited successfully", + }) + } @ApiOperation({ summary: 'add new course' }) @ApiResponse({ status: HttpStatus.CREATED, type: AddCourseResponseDto }) diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 2fcdac4..e8b62b9 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -10,6 +10,7 @@ import { Feedback, FeedbackResponseDto } from './dto/feedback.dto'; import { PurchaseResponseDto } from './dto/purchase.dto'; import { CourseDto } from 'src/course/dto/course.dto'; import { CompleteCourseDto } from 'src/course/dto/completion.dto'; +import { EditCourseDto } from 'src/course/dto/edit-course.dto'; @Injectable() export class ProviderService { @@ -111,6 +112,15 @@ export class ProviderService { return this.courseService.getProviderCourses(providerId); } + async editCourse(providerId: number, courseId: number, editCourseDto: EditCourseDto) { + const course = await this.courseService.editCourse(providerId, courseId, editCourseDto); + return course; + } + + async archiveCourse(providerId: number, courseId: number) { + return this.courseService.archiveCourse(providerId, courseId); + } + async getCourseFeedbacks(providerId: number, courseId: number): Promise { const course = await this.courseService.getCourse(courseId); From 2f5821003a54c63afec63d4ebc5caa248d54fd8e Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Tue, 7 Nov 2023 03:06:06 +0530 Subject: [PATCH 17/88] Added additional apis --- src/admin/admin.controller.ts | 54 +++++++++++++++++++ src/admin/admin.service.ts | 68 ++++++++++++++++++++++++ src/admin/dto/provider-settlement.dto.ts | 36 +++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 src/admin/dto/provider-settlement.dto.ts diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 1ea008a..a952c67 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -10,6 +10,7 @@ import { TransactionResponse } from './dto/transaction-response.dto'; import { Response } from 'express'; import { CreditRequest } from './dto/credit-request.dto'; import { json } from 'stream/consumers'; +import { ProviderSettlementDto } from './dto/provider-settlement.dto'; @Controller('admin') @ApiTags('admin') @@ -44,6 +45,58 @@ export class AdminController { } } + @ApiOperation({ summary: "Get all providers for settlement" }) + @ApiResponse({ status: HttpStatus.OK, type: ProviderSettlementDto, isArray: true}) + @Get('/providers/settlements') + async getAllProvidersForSettlement(@Res() res : Response) { + try { + this.logger.log(`Getting information of all the providers for settlement`); + + const providers = await this.adminService.getAllProviderInfoForSettlement(); + + this.logger.log(`Successfully retrieved all the provider info for making settlement`); + + res.status(HttpStatus.OK).json({ + message: "All providers fetched", + data: providers + }); + } catch (err) { + this.logger.error(`Failed to retreive all the providers' information for settlement`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch all the providers' information for settlement", + }); + } + } + + @ApiOperation({ summary: "Settle credits for a provider" }) + @ApiResponse({ status: HttpStatus.OK, type: json}) + @Post('/providers/settlements/settle') + async settleProvider(@Body() settleDto: ProviderSettlementDto, @Res() res : Response) { + try { + this.logger.log(`Settling the credits for the given provider`); + + const updatedWallet = await this.adminService.settleCredits(settleDto.id); + + this.logger.log(`Successfully settled the credits for the provider`); + + res.status(HttpStatus.OK).json({ + message: "Settlement done for the provider", + data: updatedWallet + }); + } catch (err) { + this.logger.error(`Failed to retreive all the providers' information for settlement`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch all the providers' information for settlement", + }); + } + } + @ApiOperation({ summary: "View provider profile information"}) @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse}) @Get('/providers/:providerId') @@ -361,5 +414,6 @@ export class AdminController { message: errorMessage || `Failed to remove credits from the provider`, }); } + } } diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 02c5bc7..383d838 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -163,5 +163,73 @@ export class AdminService { return updatedProfile; } + async getNoOfCoursePurchasesForProvider(providerId: number) { + return await this.prisma.userCourse.count({ + where: { + course: { + providerId: providerId + } + } + }); + } + + async getNumberOfCoursesForProvider(providerId: number) { + return await this.prisma.course.count({ + where: { + providerId: providerId + } + }); + } + + async getProviderWalletCredits(providerId: number) { + const providerWallet = await this.prisma.wallet.findFirst({ + where: { + provider: { + id: providerId + } + } + }); + + return providerWallet?.credits; + } + + async getAllProviderInfoForSettlement() { + + const providers = await this.prisma.provider.findMany({}); + const results = providers.map((provider) => { + const providerId = provider.id; + return { + id: providerId, + name: provider.name, + numberOfCourses: this.getNumberOfCoursesForProvider(providerId), + activeUsers: this.getNoOfCoursePurchasesForProvider(providerId), + totalCredits: this.getProviderWalletCredits(providerId) + } + }); + return results; + } + + async settleCredits(providerId: number) { + // Need to add transaction, add paymentReceipt additional settlement processing + // then set the credits of the provider to 0 + const wallet = await this.prisma.wallet.findFirst({ + where: { + provider: { + id: providerId + } + } + }); + + const updatedWallet = await this.prisma.wallet.update({ + where: { + walletId: wallet?.walletId + }, + data: { + credits: 0 + } + }); + return updatedWallet; + } + } diff --git a/src/admin/dto/provider-settlement.dto.ts b/src/admin/dto/provider-settlement.dto.ts new file mode 100644 index 0000000..d61e823 --- /dev/null +++ b/src/admin/dto/provider-settlement.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNumber, IsOptional, IsString } from 'class-validator'; + + +export class ProviderSettlementDto { + @ApiProperty({required: false}) + @IsNumber() + @IsOptional() + id: number; + + @ApiProperty() + @IsOptional() + @IsString() + imgLink: string; + + @ApiProperty() + @IsOptional() + @IsString() + name: string; + + @ApiProperty() + @IsNumber() + @IsOptional() + totalCourses: number; + + @ApiProperty() + @IsNumber() + @IsOptional() + activeUsers: number; + + @ApiProperty() + @IsNumber() + @IsOptional() + totalCredits: number; + +} \ No newline at end of file From 61fe529ffcf7350a4cef80c6ab0d3ccb649c15da Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Tue, 7 Nov 2023 16:48:24 +0530 Subject: [PATCH 18/88] course module fixes --- src/app.module.ts | 3 ++- src/course/course.controller.ts | 5 ++-- src/course/course.module.ts | 6 +++-- src/course/course.service.ts | 38 ++++++++++++++++++----------- src/provider/provider.controller.ts | 3 ++- 5 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index 71eefc9..eb6af56 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,9 +4,9 @@ import { AppController } from "./app.controller"; import { AppService } from "./app.service"; import { PrismaModule } from "./prisma/prisma.module"; import { ProviderModule } from "./provider/provider.module"; -import { PrismaService } from "./prisma/prisma.service"; import { AdminModule } from './admin/admin.module'; import { MockWalletModule } from './mock-wallet/mock-wallet.module'; +import { CourseModule } from "./course/course.module"; @Module({ imports: [ @@ -17,6 +17,7 @@ import { MockWalletModule } from './mock-wallet/mock-wallet.module'; ProviderModule, AdminModule, MockWalletModule, + CourseModule ], controllers: [AppController], providers: [AppService], diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index 5f47bad..c7b2e43 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -1,10 +1,11 @@ import { Body, Controller, Get, HttpStatus, Param, ParseIntPipe, Patch, Post, Res } from "@nestjs/common"; -import { ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { CourseService } from "./course.service"; import { CourseDto } from "./dto/course.dto"; import { FeedbackDto } from "./dto/feedback.dto"; @Controller('course') +@ApiTags('course') export class CourseController { constructor(private readonly courseService: CourseService) {} @@ -58,7 +59,7 @@ export class CourseController { @ApiOperation({ summary: 'Give feedback and rating' }) @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:courseId/purchase/:userId") + @Patch("/:courseId/feedback/:userId") // Give feedback and rating async feedback( @Param("courseId", ParseIntPipe) courseId: number, diff --git a/src/course/course.module.ts b/src/course/course.module.ts index 99f66b0..520c742 100644 --- a/src/course/course.module.ts +++ b/src/course/course.module.ts @@ -1,8 +1,10 @@ import { Module } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; +import { CourseService } from "./course.service"; +import { CourseController } from "./course.controller"; @Module({ - controllers: [], - providers: [PrismaService], + controllers: [CourseController], + providers: [PrismaService, CourseService], }) export class CourseModule {} diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 3a73acd..f53d4e0 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -115,22 +115,32 @@ export class CourseService { async giveCourseFeedback(courseId: number, userId: string, feedbackDto: FeedbackDto) { await this.getCourse(courseId); - - try { - await this.prisma.userCourse.update({ - where: { - userId_courseId: { - courseId, - userId - } - }, - data: { - ...feedbackDto + + const userCourse = await this.prisma.userCourse.findUnique({ + where: { + userId_courseId: { + userId, + courseId: courseId } - }); - } catch { + }, + }); + if(!userCourse) throw new NotFoundException("This user has not subscribed to this course"); - } + + if(userCourse.status != CourseProgressStatus.completed) + throw new BadRequestException("Course not complete"); + + await this.prisma.userCourse.update({ + where: { + userId_courseId: { + courseId, + userId + } + }, + data: { + ...feedbackDto + } + }); } async deleteCourse(courseId: number) { diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index 3d0c092..b943097 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Delete, Get, HttpStatus, Param, ParseIntPipe, Patch, Post, Put, Res } from '@nestjs/common'; import { ProviderService } from './provider.service'; -import { ApiOperation, ApiResponse } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SignupDto, SignupResponseDto } from './dto/signup.dto'; import { LoginDto, LoginResponseDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; @@ -13,6 +13,7 @@ import { CompleteCourseDto } from 'src/course/dto/completion.dto'; import { EditCourseDto } from 'src/course/dto/edit-course.dto'; @Controller('provider') +@ApiTags('provider') export class ProviderController { constructor( private providerService: ProviderService, From 68e30f2c7046d40096fe7a6a6858b388bf212a9e Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Wed, 8 Nov 2023 00:41:47 +0530 Subject: [PATCH 19/88] refactoring --- src/admin/admin.controller.spec.ts | 2 +- src/admin/admin.controller.ts | 6 +- src/admin/admin.module.ts | 4 +- src/admin/admin.service.ts | 106 ++++-------------- src/admin/dto/edit-provider.dto.ts | 2 +- src/course/course.controller.ts | 8 +- src/course/course.service.ts | 49 +++++++- .../dto/course-response.dto.ts | 18 +-- src/course/dto/course.dto.ts | 21 ---- .../dto/verify-course.dto.ts | 0 .../dto/provider-profile-response.dto.ts | 4 +- src/provider/dto/view-profile.dto.ts | 12 -- src/provider/provider.controller.ts | 8 +- src/provider/provider.service.ts | 41 +++++-- 14 files changed, 130 insertions(+), 151 deletions(-) rename src/{admin => course}/dto/course-response.dto.ts (63%) delete mode 100644 src/course/dto/course.dto.ts rename src/{admin => course}/dto/verify-course.dto.ts (100%) rename src/{admin => provider}/dto/provider-profile-response.dto.ts (84%) delete mode 100644 src/provider/dto/view-profile.dto.ts diff --git a/src/admin/admin.controller.spec.ts b/src/admin/admin.controller.spec.ts index baf19ac..70587e0 100644 --- a/src/admin/admin.controller.spec.ts +++ b/src/admin/admin.controller.spec.ts @@ -4,7 +4,7 @@ import { AdminService } from './admin.service'; import { PrismaModule } from 'nestjs-prisma'; import { PrismaService } from '../prisma/prisma.service'; import { ConfigService } from '@nestjs/config'; -import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; +import { ProviderProfileResponse } from '../provider/dto/provider-profile-response.dto'; describe('AdminController', () => { let controller: AdminController; diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index a952c67..340a071 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -1,16 +1,16 @@ import { Controller, Body, Get, Post, Patch, Res, Delete, HttpStatus, Param, ParseIntPipe, Logger} from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { AdminService } from './admin.service'; -import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; +import { ProviderProfileResponse } from '../provider/dto/provider-profile-response.dto'; import { getPrismaErrorStatusAndMessage } from '../utils/utils'; import { EditProvider } from './dto/edit-provider.dto'; -import { CourseResponse } from './dto/course-response.dto'; -import { CourseVerify } from './dto/verify-course.dto'; +import { CourseResponse } from '../course/dto/course-response.dto'; import { TransactionResponse } from './dto/transaction-response.dto'; import { Response } from 'express'; import { CreditRequest } from './dto/credit-request.dto'; import { json } from 'stream/consumers'; import { ProviderSettlementDto } from './dto/provider-settlement.dto'; +import { CourseVerify } from 'src/course/dto/verify-course.dto'; @Controller('admin') @ApiTags('admin') diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts index eed8ad6..8e81acd 100644 --- a/src/admin/admin.module.ts +++ b/src/admin/admin.module.ts @@ -5,10 +5,12 @@ import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaModule } from 'nestjs-prisma'; import { MockWalletModule } from 'src/mock-wallet/mock-wallet.module'; import { MockWalletService } from 'src/mock-wallet/mock-wallet.service'; +import { ProviderService } from 'src/provider/provider.service'; +import { CourseService } from 'src/course/course.service'; @Module({ imports: [PrismaModule, MockWalletModule], controllers: [AdminController], - providers: [AdminService, PrismaService, MockWalletService] + providers: [AdminService, PrismaService, MockWalletService, ProviderService, CourseService] }) export class AdminModule {} diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 383d838..8604fbd 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -4,110 +4,57 @@ import { PrismaService } from '../prisma/prisma.service'; import { Provider, ProviderStatus, Course, CourseVerificationStatus } from '@prisma/client'; import { EditProvider } from './dto/edit-provider.dto'; import { MockWalletService } from '../mock-wallet/mock-wallet.service'; +import { ProviderService } from 'src/provider/provider.service'; +import { CourseService } from 'src/course/course.service'; @Injectable() export class AdminService { - constructor(private prisma: PrismaService, private wallet: MockWalletService) {} + constructor( + private prisma: PrismaService, + private wallet: MockWalletService, + private providerService: ProviderService, + private courseService: CourseService + ) {} async verifyProvider(providerId: number) { - let providerInfo = await this.prisma.provider.findUnique({ - where: { id: providerId } - }); - - if(!providerInfo) { - throw new NotFoundException(`Provider with id ${providerId} does not exist`); - } - - if(providerInfo.status != ProviderStatus.pending) { - throw new HttpException(`Provider is either verified or rejected.`, 406); - } - const updatedRecord = await this.prisma.provider.update({ - where: {id: providerId}, - data: {status: ProviderStatus.verified} - }); - return updatedRecord; + return this.providerService.verifyProvider(providerId); } async findAllProviders(): Promise[]> { - let providers = await this.prisma.provider.findMany({ - select: { id: true, name: true, email: true, walletId: true, paymentInfo: true, courses: true, status: true} - }); - return providers; + + return this.providerService.fetchAllProviders(); } async findProviderById(providerId: number): Promise { - let provider = await this.prisma.provider.findFirst({ - where: {id: providerId} - }); - - if(!provider) { - throw new NotFoundException(`Provider with id ${providerId} does not exist`); - } - return provider; + return this.providerService.getProvider(providerId); } async findAllCourses() : Promise { - let courses = this.prisma.course.findMany(); - return courses; + + return this.courseService.fetchAllCourses(); } async findCourseById(courseId: number) { - let course = await this.prisma.course.findUnique({ - where: { id: courseId } - }); - if(!course) { - throw new NotFoundException(`Course with id ${courseId} not found`); - } - return course; + + return this.courseService.getCourse(courseId); } async acceptCourse(courseId: number, cqf_score: number) { - let course = await this.prisma.course.findUnique({ - where: { id: courseId } - }); - if(!course) { - throw new NotFoundException(`Course with id ${courseId} not found`); - } - if(course.verificationStatus != CourseVerificationStatus.pending) { - throw new HttpException(`Course is either rejected or is already accepted.`, 406); - } - let updatedCourse = await this.prisma.course.update({ - where: { id: courseId }, - data: { - verificationStatus: CourseVerificationStatus.accepted, - cqfScore: cqf_score - } - }); - return updatedCourse; + + return this.courseService.acceptCourse(courseId, cqf_score); } async rejectCourse(courseId: number) { - let course = await this.prisma.course.findUnique({ - where: { id: courseId } - }); - if(!course) { - throw new NotFoundException(`Course with id ${courseId} not found`); - } - let updatedCourse = await this.prisma.course.update({ - where: {id: courseId}, - data: {verificationStatus: CourseVerificationStatus.rejected} - }); - return updatedCourse; + + return this.courseService.rejectCourse(courseId); } async removeCourse(courseId: number) { - let course = await this.prisma.course.findUnique({ - where: { id: courseId } - }); - if(!course) { - throw new NotFoundException(`Course with id ${courseId} not found`); - } - await this.prisma.course.delete({ - where: {id: courseId} - }); + + return this.courseService.removeCourse(courseId); } async getTransactions(adminId: number) { @@ -155,12 +102,7 @@ export class AdminService { async editProviderProfile(profileInfo: EditProvider) { - const updatedProfile = await this.prisma.provider.update({ - where: { id: profileInfo.id }, - data: profileInfo - }); - - return updatedProfile; + return this.providerService.editProviderProfileByAdmin(profileInfo); } async getNoOfCoursePurchasesForProvider(providerId: number) { @@ -195,7 +137,7 @@ export class AdminService { async getAllProviderInfoForSettlement() { - const providers = await this.prisma.provider.findMany({}); + const providers = await this.providerService.fetchAllProviders(); const results = providers.map((provider) => { const providerId = provider.id; return { diff --git a/src/admin/dto/edit-provider.dto.ts b/src/admin/dto/edit-provider.dto.ts index cc7b2f4..e90e466 100644 --- a/src/admin/dto/edit-provider.dto.ts +++ b/src/admin/dto/edit-provider.dto.ts @@ -1,5 +1,5 @@ import { PartialType } from "@nestjs/swagger"; -import { ProviderProfileResponse } from "./provider-profile-response.dto"; +import { ProviderProfileResponse } from "../../provider/dto/provider-profile-response.dto"; export class EditProvider extends PartialType(ProviderProfileResponse){ } \ No newline at end of file diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index c7b2e43..4d4bd96 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -1,8 +1,8 @@ import { Body, Controller, Get, HttpStatus, Param, ParseIntPipe, Patch, Post, Res } from "@nestjs/common"; import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { CourseService } from "./course.service"; -import { CourseDto } from "./dto/course.dto"; import { FeedbackDto } from "./dto/feedback.dto"; +import { CourseResponse } from "./dto/course-response.dto"; @Controller('course') @ApiTags('course') @@ -10,7 +10,7 @@ export class CourseController { constructor(private readonly courseService: CourseService) {} @ApiOperation({ summary: 'Search courses' }) - @ApiResponse({ status: HttpStatus.OK, type: [CourseDto] }) + @ApiResponse({ status: HttpStatus.OK, type: [CourseResponse] }) @Get("/search") // search courses async searchCourses( @@ -26,14 +26,14 @@ export class CourseController { } @ApiOperation({ summary: 'Fetch details of one course' }) - @ApiResponse({ status: HttpStatus.OK, type: CourseDto }) + @ApiResponse({ status: HttpStatus.OK, type: CourseResponse }) @Get("/:courseId") // Fetch details of one course async getCourse( @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { - const course = await this.courseService.getCourse(courseId); + const course: CourseResponse = await this.courseService.getCourse(courseId); res.status(HttpStatus.OK).json({ message: "fetch successful", diff --git a/src/course/course.service.ts b/src/course/course.service.ts index f53d4e0..7af83fd 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -1,11 +1,11 @@ -import { NotFoundException, BadRequestException, Injectable } from "@nestjs/common"; +import { NotFoundException, BadRequestException, Injectable, HttpException } from "@nestjs/common"; import { PrismaService } from "src/prisma/prisma.service"; import { FeedbackDto } from "./dto/feedback.dto"; -import { CourseDto } from "./dto/course.dto"; import { AddCourseDto } from "./dto/add-course.dto"; -import { CourseProgressStatus, CourseStatus, CourseVerificationStatus } from "@prisma/client"; +import { Course, CourseProgressStatus, CourseStatus, CourseVerificationStatus } from "@prisma/client"; import { CompleteCourseDto } from "./dto/completion.dto"; import { EditCourseDto } from "./dto/edit-course.dto"; +import { AdminCourseResponse, CourseResponse } from "src/course/dto/course-response.dto"; @Injectable() export class CourseService { @@ -13,7 +13,7 @@ export class CourseService { private prisma: PrismaService, ) {} - async searchCourses(): Promise { + async searchCourses(): Promise { const courses = await this.prisma.course.findMany({ // where: { @@ -87,7 +87,7 @@ export class CourseService { }) } - async getCourse(courseId: number): Promise { + async getCourse(courseId: number): Promise { const course = await this.prisma.course.findUnique({ where: { @@ -185,4 +185,43 @@ export class CourseService { }) } + async fetchAllCourses() : Promise { + + return this.prisma.course.findMany(); + } + + async acceptCourse(courseId: number, cqf_score: number) { + + let course = await this.getCourse(courseId); + + if(course.verificationStatus != CourseVerificationStatus.pending) { + throw new HttpException(`Course is either rejected or is already accepted.`, 406); + } + return this.prisma.course.update({ + where: { id: courseId }, + data: { + verificationStatus: CourseVerificationStatus.accepted, + cqfScore: cqf_score + } + }); + } + + async rejectCourse(courseId: number) { + + await this.getCourse(courseId); + + return this.prisma.course.update({ + where: {id: courseId}, + data: {verificationStatus: CourseVerificationStatus.rejected} + }); + } + + async removeCourse(courseId: number) { + + await this.getCourse(courseId); + + return this.prisma.course.delete({ + where: {id: courseId} + }); + } } diff --git a/src/admin/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts similarity index 63% rename from src/admin/dto/course-response.dto.ts rename to src/course/dto/course-response.dto.ts index e56f457..9de456a 100644 --- a/src/admin/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -1,7 +1,7 @@ import { CourseStatus, CourseVerificationStatus } from "@prisma/client"; export class CourseResponse { - + readonly id: number; readonly providerId: number; readonly title: string; @@ -9,15 +9,19 @@ export class CourseResponse { readonly courseLink: string; readonly imgLink: string; readonly credits: number; - readonly noOfLessons: number; + readonly noOfLessons: number | null; readonly language: string[]; readonly duration: number; - readonly competencies: JSON; + readonly competency: any; readonly author: string; - readonly avgRating: number; + readonly avgRating: number | null; readonly status: CourseStatus; - readonly availabilityTime: Date; + readonly availabilityTime: Date | null; readonly verificationStatus: CourseVerificationStatus; - readonly cqf_score: number; - readonly impact_score: number; +} + +export class AdminCourseResponse extends CourseResponse { + + readonly cqfScore: number | null; + readonly impactScore: number | null; } \ No newline at end of file diff --git a/src/course/dto/course.dto.ts b/src/course/dto/course.dto.ts deleted file mode 100644 index b2e8b15..0000000 --- a/src/course/dto/course.dto.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { $Enums, Prisma } from "@prisma/client"; - -export class CourseDto { - - readonly id: number; - readonly providerId: number; - readonly title: string; - readonly description: string; - readonly courseLink: string; - readonly imgLink: string; - readonly credits: number; - readonly noOfLessons: number | null; - readonly language: string[]; - readonly duration: number; - readonly competency: any; - readonly author: string; - readonly avgRating: number | null; - readonly status: $Enums.CourseStatus; - readonly availabilityTime: Date | null; - readonly verificationStatus: $Enums.CourseVerificationStatus; -} \ No newline at end of file diff --git a/src/admin/dto/verify-course.dto.ts b/src/course/dto/verify-course.dto.ts similarity index 100% rename from src/admin/dto/verify-course.dto.ts rename to src/course/dto/verify-course.dto.ts diff --git a/src/admin/dto/provider-profile-response.dto.ts b/src/provider/dto/provider-profile-response.dto.ts similarity index 84% rename from src/admin/dto/provider-profile-response.dto.ts rename to src/provider/dto/provider-profile-response.dto.ts index 59d1c24..07bd10e 100644 --- a/src/admin/dto/provider-profile-response.dto.ts +++ b/src/provider/dto/provider-profile-response.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { ProviderStatus } from "@prisma/client"; -import { IsEmail, IsEnum, IsJSON, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsEmail, IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'; export class ProviderProfileResponse { @@ -22,7 +22,7 @@ export class ProviderProfileResponse { @ApiProperty() @IsString() @IsOptional() - password: string; + password?: string; @ApiProperty() @IsNumber() diff --git a/src/provider/dto/view-profile.dto.ts b/src/provider/dto/view-profile.dto.ts deleted file mode 100644 index b8cd605..0000000 --- a/src/provider/dto/view-profile.dto.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { $Enums, Prisma } from "@prisma/client"; - -export class ViewProfileResponseDto { - - readonly id: number; - readonly name: string; - readonly email: string; - readonly password: string; - readonly walletId: number; - readonly paymentInfo: Prisma.JsonValue; - readonly status: $Enums.ProviderStatus; -} diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index b943097..f280641 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -5,12 +5,12 @@ import { SignupDto, SignupResponseDto } from './dto/signup.dto'; import { LoginDto, LoginResponseDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { AddCourseDto, AddCourseResponseDto } from 'src/course/dto/add-course.dto'; -import { ViewProfileResponseDto } from './dto/view-profile.dto'; import { FeedbackResponseDto } from './dto/feedback.dto'; import { PurchaseResponseDto } from './dto/purchase.dto'; -import { CourseDto } from 'src/course/dto/course.dto'; import { CompleteCourseDto } from 'src/course/dto/completion.dto'; import { EditCourseDto } from 'src/course/dto/edit-course.dto'; +import { CourseResponse } from 'src/course/dto/course-response.dto'; +import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; @Controller('provider') @ApiTags('provider') @@ -56,7 +56,7 @@ export class ProviderController { } @ApiOperation({ summary: 'view provider profile' }) - @ApiResponse({ status: HttpStatus.OK, type: ViewProfileResponseDto }) + @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse }) @Get("/:providerId/profile") // view provider profile information async viewProfile( @@ -154,7 +154,7 @@ export class ProviderController { } @ApiOperation({ summary: 'View courses offered by self' }) - @ApiResponse({ status: HttpStatus.OK, type: [CourseDto] }) + @ApiResponse({ status: HttpStatus.OK, type: [CourseResponse] }) @Get("/:providerId/course") // View courses offered by self async fetchProviderCourses( diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index e8b62b9..6a4779b 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -1,16 +1,18 @@ -import { BadRequestException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, HttpException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { SignupDto } from './dto/signup.dto'; import { PrismaService } from 'src/prisma/prisma.service'; -import { Prisma, ProviderStatus, WalletType } from '@prisma/client'; +import { Prisma, Provider, ProviderStatus, WalletType } from '@prisma/client'; import { LoginDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { AddCourseDto } from 'src/course/dto/add-course.dto'; import { CourseService } from 'src/course/course.service'; import { Feedback, FeedbackResponseDto } from './dto/feedback.dto'; import { PurchaseResponseDto } from './dto/purchase.dto'; -import { CourseDto } from 'src/course/dto/course.dto'; import { CompleteCourseDto } from 'src/course/dto/completion.dto'; import { EditCourseDto } from 'src/course/dto/edit-course.dto'; +import { EditProvider } from 'src/admin/dto/edit-provider.dto'; +import { CourseResponse } from 'src/course/dto/course-response.dto'; +import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; @Injectable() export class ProviderService { @@ -85,6 +87,14 @@ export class ProviderService { } } + async editProviderProfileByAdmin(profileInfo: EditProvider) { + + return this.prisma.provider.update({ + where: { id: profileInfo.id }, + data: profileInfo + }); + } + async addNewCourse(providerId: number, addCourseDto: AddCourseDto) { const provider = await this.getProvider(providerId); @@ -107,7 +117,7 @@ export class ProviderService { await this.courseService.deleteCourse(courseId); } - async getCourses(providerId: number): Promise { + async getCourses(providerId: number): Promise { return this.courseService.getProviderCourses(providerId); } @@ -124,8 +134,6 @@ export class ProviderService { async getCourseFeedbacks(providerId: number, courseId: number): Promise { const course = await this.courseService.getCourse(courseId); - if(!course) - throw new NotFoundException("Course does not exist"); if(course.providerId != providerId) throw new BadRequestException("Course does not belong to this provider"); @@ -150,8 +158,6 @@ export class ProviderService { async getCoursePurchases(providerId: number, courseId: number): Promise { const course = await this.courseService.getCourse(courseId); - if(!course) - throw new NotFoundException("Course does not exist"); if(course.providerId != providerId) throw new BadRequestException("Course does not belong to this provider"); @@ -183,4 +189,23 @@ export class ProviderService { throw new NotFoundException("This user has not subscribed to this course"); } } + + async fetchAllProviders(): Promise { + + return this.prisma.provider.findMany({ + select: { id: true, name: true, email: true, walletId: true, paymentInfo: true, courses: true, status: true} + }); + } + + async verifyProvider(providerId: number) { + let providerInfo = await this.getProvider(providerId); + + if(providerInfo.status != ProviderStatus.pending) { + throw new HttpException(`Provider is either verified or rejected.`, 406); + } + return this.prisma.provider.update({ + where: {id: providerId}, + data: {status: ProviderStatus.verified} + }); + } } \ No newline at end of file From d12335a21cc411fd305d928f2976d669191d32f9 Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Tue, 14 Nov 2023 13:06:52 +0530 Subject: [PATCH 20/88] Addressed review --- env-example | 1 + package-lock.json | 12 ++- package.json | 1 + prisma/schema.prisma | 33 ++------ prisma/seed.ts | 59 +++++--------- src/admin/admin.controller.ts | 7 +- src/admin/admin.module.ts | 9 +-- src/admin/admin.service.ts | 97 ++++++++++-------------- src/admin/dto/credit-request.dto.ts | 4 +- src/admin/dto/provider-settlement.dto.ts | 11 ++- src/course/course.module.ts | 1 + src/course/course.service.ts | 2 +- src/mock-wallet/mock-wallet.module.ts | 3 +- src/prisma/prisma.module.ts | 3 +- 14 files changed, 87 insertions(+), 156 deletions(-) diff --git a/env-example b/env-example index b623ba8..eddeb12 100644 --- a/env-example +++ b/env-example @@ -8,3 +8,4 @@ DATABASE_PASSWORD= DATABASE_NAME= DATABASE_PORT=5432 DATABASE_URL= +WALLET_SERVICE_URL= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 84dc5b8..e9ea5ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^7.1.14", "@prisma/client": "^5.4.1", + "axios": "^1.6.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "lodash": "^4.17.21", @@ -2840,10 +2841,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz", - "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==", - "peer": true, + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", + "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -4428,7 +4428,6 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], - "peer": true, "engines": { "node": ">=4.0" }, @@ -6806,8 +6805,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "peer": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/pump": { "version": "3.0.0", diff --git a/package.json b/package.json index 35302d5..6a0d768 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@nestjs/platform-express": "^9.0.0", "@nestjs/swagger": "^7.1.14", "@prisma/client": "^5.4.1", + "axios": "^1.6.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "lodash": "^4.17.21", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e313158..947f121 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -36,7 +36,7 @@ enum CourseProgressStatus { } model Admin { - id Int @id @default(autoincrement()) + id String @id @default(uuid()) name String email String @unique password String @@ -44,7 +44,7 @@ model Admin { } model Provider { - id Int @id @default(autoincrement()) + id String @id @default(uuid()) name String email String @unique password String @@ -52,35 +52,12 @@ model Provider { paymentInfo Json? status ProviderStatus @default(pending) courses Course[] - wallet Wallet @relation(fields: [walletId], references: [walletId]) } -// dummy wallet model -model Wallet { - walletId Int @id @default(autoincrement()) - type WalletType - status WalletStatus @default(active) - credits Int @default(0) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - provider Provider? -} - -enum WalletType { - admin - provider - consumer -} - -enum WalletStatus { - active - inactive - frozen -} model Course { id Int @id @default(autoincrement()) - providerId Int + providerId String title String description String courseLink String @@ -88,7 +65,7 @@ model Course { credits Int noOfLessons Int? language String[] - duration Int + duration Float competency Json author String avgRating Float? @@ -103,7 +80,7 @@ model Course { model UserCourse { id Int @id @default(autoincrement()) - userId String + userId String @db.Uuid courseId Int purchasedAt DateTime @default(now()) status CourseProgressStatus @default(inProgress) diff --git a/prisma/seed.ts b/prisma/seed.ts index 6993921..2dc4730 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -8,11 +8,7 @@ async function main() { name: "Vijay Salgaonkar", email: "vijaysalgaonkar@gmail.com", password: "9d209aacaed4088d68c41bd8dfb20de39cbd8339", - wallet: { - create: { - type: 'provider', - } - }, + walletId: 1, status: ProviderStatus.verified } }); @@ -22,11 +18,7 @@ async function main() { name: "udemy", email: "udemyorg@gmail.in", password: "Udemy@9812", - wallet: { - create: { - type: 'provider', - } - }, + walletId: 2, paymentInfo: { bankAccNo: "1111111111", otherDetails: { @@ -43,11 +35,7 @@ async function main() { name: "coursera", email: "coursera@gmail.in", password: "Coursera@999", - wallet: { - create: { - type: 'provider', - } - }, + walletId: 3, paymentInfo: { bankAccNo: "1111111113", otherDetails: { @@ -64,11 +52,7 @@ async function main() { name: "lern", email: "lern@gmail.in", password: "lern@999", - wallet: { - create: { - type: 'provider', - } - }, + walletId: 4, paymentInfo: { bankAccNo: "1111111116", otherDetails: { @@ -153,11 +137,8 @@ async function main() { }] }) - const admin1 = await prisma.admin.upsert({ - where: { id: 1 }, - update: {}, - create: { - id: 1, + const admin1 = await prisma.admin.create({ + data: { name: 'admin1', email: "admin1@gmail.com", password: "123456", @@ -165,11 +146,8 @@ async function main() { }, }); - const admin2 = await prisma.admin.upsert({ - where: { id: 2 }, - update: {}, - create: { - id: 2, + const admin2 = await prisma.admin.create({ + data: { name: 'admin2', email: "admin2@gmail.com", password: "admin", @@ -177,11 +155,8 @@ async function main() { }, }); - const admin3 = await prisma.admin.upsert({ - where: { id: 3 }, - update: {}, - create: { - id: 3, + const admin3 = await prisma.admin.create({ + data: { name: 'admin3', email: "admin3@gmail.com", password: "admin3", @@ -193,7 +168,7 @@ async function main() { const course1 = await prisma.course.create({ data: { - providerId: 1, + providerId: provider1.id, title: "Learn DevOps & Kubernetes", description: "This course enables anyone to get started with devops engineering.", courseLink: "https://udemy.com/courses/pYUxbhj", @@ -217,7 +192,7 @@ async function main() { const course2 = await prisma.course.create({ data: { - providerId: 1, + providerId: provider1.id, title: "Introduction to Programming", description: "This course covers all the fundamentals of programming", courseLink: "https://udemy.com/courses/jQKsLpm", @@ -239,7 +214,7 @@ async function main() { const course3 = await prisma.course.create({ data: { - providerId: 1, + providerId: provider1.id, title: "Introduction to Compiler Engineering", description: "This course covers how compilers are built and also teaches you about how to create custom programming languages", courseLink: "https://udemy.com/courses/jQKsLpm", @@ -263,22 +238,22 @@ async function main() { console.log("All courses: ", resp); const response3 = await prisma.userCourse.createMany({ data: [{ - userId: "56e2b", + userId: "c2cc3f08-b6fc-4d53-aa91-2bfcb4e0a5c1", feedback: "Great course", rating: 4, courseId: 1 }, { - userId: "c8200", + userId: "8d1f5e46-4e0d-401e-83b4-5a72fbd6c5a9", feedback: "Instructor is very friendly", rating: 4, courseId: 2 }, { - userId: "f9464", + userId: "a3a5f480-9ac1-4e20-b0d9-7b3a662e2c36", feedback: "Some more real world applications could be discussed", rating: 3, courseId: 2 }, { - userId: "91e61", + userId: "f9b69f4b-1095-4d29-9f49-8f653eb5b3bd", feedback: "Not satisfied with the content", rating: 2, courseId: 3 diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 340a071..ec1ab5f 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -73,18 +73,17 @@ export class AdminController { @ApiOperation({ summary: "Settle credits for a provider" }) @ApiResponse({ status: HttpStatus.OK, type: json}) - @Post('/providers/settlements/settle') - async settleProvider(@Body() settleDto: ProviderSettlementDto, @Res() res : Response) { + @Post('/:adminId/providers/settlements/settle') + async settleProvider(@Param("adminId", ParseIntPipe) adminId: number, @Body() settleDto: ProviderSettlementDto, @Res() res : Response) { try { this.logger.log(`Settling the credits for the given provider`); - const updatedWallet = await this.adminService.settleCredits(settleDto.id); + await this.adminService.settleCredits(adminId, settleDto.id); this.logger.log(`Successfully settled the credits for the provider`); res.status(HttpStatus.OK).json({ message: "Settlement done for the provider", - data: updatedWallet }); } catch (err) { this.logger.error(`Failed to retreive all the providers' information for settlement`); diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts index 8e81acd..3c1c647 100644 --- a/src/admin/admin.module.ts +++ b/src/admin/admin.module.ts @@ -4,13 +4,12 @@ import { AdminService } from './admin.service'; import { PrismaService } from 'src/prisma/prisma.service'; import { PrismaModule } from 'nestjs-prisma'; import { MockWalletModule } from 'src/mock-wallet/mock-wallet.module'; -import { MockWalletService } from 'src/mock-wallet/mock-wallet.service'; -import { ProviderService } from 'src/provider/provider.service'; -import { CourseService } from 'src/course/course.service'; +import { CourseModule } from 'src/course/course.module'; +import { ProviderModule } from 'src/provider/provider.module'; @Module({ - imports: [PrismaModule, MockWalletModule], + imports: [PrismaModule, CourseModule, MockWalletModule, ProviderModule], controllers: [AdminController], - providers: [AdminService, PrismaService, MockWalletService, ProviderService, CourseService] + providers: [AdminService, PrismaService] }) export class AdminModule {} diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 8604fbd..fe4bb63 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -1,11 +1,12 @@ import { HttpException, Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; -import { Provider, ProviderStatus, Course, CourseVerificationStatus } from '@prisma/client'; +import { Provider, Course } from '@prisma/client'; import { EditProvider } from './dto/edit-provider.dto'; import { MockWalletService } from '../mock-wallet/mock-wallet.service'; import { ProviderService } from 'src/provider/provider.service'; import { CourseService } from 'src/course/course.service'; +import axios from 'axios'; @Injectable() export class AdminService { @@ -59,14 +60,12 @@ export class AdminService { async getTransactions(adminId: number) { - // const walletService = process.env.WALLET_SERVICE_URL; - // const endpoint = `/admin/${adminId}/transactions/consumers`; - // const url = walletService + endpoint; - + const walletService = process.env.WALLET_SERVICE_URL; + const endpoint = `/admin/${adminId}/transactions/consumers`; + const url = walletService + endpoint; try { - // const response: AxiosResponse = await axios.get(url); - const transactions = this.wallet.getTransactions(adminId); - return transactions.data; + const response = await axios.get(url); + return response.data; } catch (err) { throw new Error(`Failed to fetch data: ${err.message}`); @@ -74,29 +73,23 @@ export class AdminService { } async addOrRemoveCreditsToProvider(adminId: number, providerId: number, credits: number) { - // const walletService = process.env.WALLET_SERVICE_URL; - // let endpoint: string; - // if(credits >= 0) { - // endpoint = `/admin/${adminId}/add-credits`; - // } else { - // endpoint = `/admin/${adminId}/reduce-credits`; - // } - // const url = walletService + endpoint; - // const requestBody = { - // consumerId: providerId, - // credits: credits - // }; + const walletService = process.env.WALLET_SERVICE_URL; + let endpoint: string; + if(credits >= 0) { + endpoint = `/admin/${adminId}/add-credits`; + } else { + endpoint = `/admin/${adminId}/reduce-credits`; + } + const url = walletService + endpoint; + const requestBody = { + consumerId: providerId, + credits: credits + }; try { - // const response = await axios.post(url, requestBody); - let response; - if(credits >= 0) { - response = this.wallet.addCredits(adminId, providerId, credits); - } else { - response = this.wallet.reduceCredits(adminId, providerId, credits); - } + let response = await axios.post(url, requestBody); return response; } catch (err) { - throw new Error(`Failed to send POST request to walletService.`); + throw new Error(`Failed to send add/reduce credits POST request to walletService.`); } } @@ -124,53 +117,39 @@ export class AdminService { } async getProviderWalletCredits(providerId: number) { - const providerWallet = await this.prisma.wallet.findFirst({ - where: { - provider: { - id: providerId - } - } - }); - - return providerWallet?.credits; + const url = process.env.WALLET_SERVICE_URL; + const endpoint = url + `/api/providers/${providerId}/credits`; + const resp = await axios.get(endpoint); } async getAllProviderInfoForSettlement() { const providers = await this.providerService.fetchAllProviders(); - const results = providers.map((provider) => { + const results = providers.map(async (provider) => { const providerId = provider.id; return { id: providerId, name: provider.name, - numberOfCourses: this.getNumberOfCoursesForProvider(providerId), - activeUsers: this.getNoOfCoursePurchasesForProvider(providerId), - totalCredits: this.getProviderWalletCredits(providerId) + numberOfCourses: await this.getNumberOfCoursesForProvider(providerId), + activeUsers: await this.getNoOfCoursePurchasesForProvider(providerId), + totalCredits: await this.getProviderWalletCredits(providerId) } }); return results; } - async settleCredits(providerId: number) { + async settleCredits(adminId: number, providerId: number) { // Need to add transaction, add paymentReceipt additional settlement processing // then set the credits of the provider to 0 - const wallet = await this.prisma.wallet.findFirst({ - where: { - provider: { - id: providerId - } - } - }); - - const updatedWallet = await this.prisma.wallet.update({ - where: { - walletId: wallet?.walletId - }, - data: { - credits: 0 - } - }); - return updatedWallet; + const totCredits = await this.getProviderWalletCredits(providerId); + const url = process.env.WALLET_SERVICE_URL; + const endpoint = url + `/api/providers/${providerId}/settlement-transaction`; + const reqBody = { + adminId: adminId, + credits: totCredits + }; + const response = await axios.post(endpoint, reqBody); + return; } } diff --git a/src/admin/dto/credit-request.dto.ts b/src/admin/dto/credit-request.dto.ts index 2b69949..9eac012 100644 --- a/src/admin/dto/credit-request.dto.ts +++ b/src/admin/dto/credit-request.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsEmail, IsJSON, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsEmail, IsInt, IsJSON, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; export class CreditRequest { @@ -10,6 +10,6 @@ export class CreditRequest { @ApiProperty() @IsNotEmpty() - @IsNumber() + @IsInt() readonly credits: number } \ No newline at end of file diff --git a/src/admin/dto/provider-settlement.dto.ts b/src/admin/dto/provider-settlement.dto.ts index d61e823..1dae033 100644 --- a/src/admin/dto/provider-settlement.dto.ts +++ b/src/admin/dto/provider-settlement.dto.ts @@ -5,32 +5,31 @@ import { IsNumber, IsOptional, IsString } from 'class-validator'; export class ProviderSettlementDto { @ApiProperty({required: false}) @IsNumber() - @IsOptional() id: number; @ApiProperty() @IsOptional() @IsString() - imgLink: string; + imgLink?: string; @ApiProperty() @IsOptional() @IsString() - name: string; + name?: string; @ApiProperty() @IsNumber() @IsOptional() - totalCourses: number; + totalCourses?: number; @ApiProperty() @IsNumber() @IsOptional() - activeUsers: number; + activeUsers?: number; @ApiProperty() @IsNumber() @IsOptional() - totalCredits: number; + totalCredits?: number; } \ No newline at end of file diff --git a/src/course/course.module.ts b/src/course/course.module.ts index 520c742..643a3c1 100644 --- a/src/course/course.module.ts +++ b/src/course/course.module.ts @@ -6,5 +6,6 @@ import { CourseController } from "./course.controller"; @Module({ controllers: [CourseController], providers: [PrismaService, CourseService], + exports: [CourseService] }) export class CourseModule {} diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 7af83fd..20caf81 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -1,5 +1,5 @@ import { NotFoundException, BadRequestException, Injectable, HttpException } from "@nestjs/common"; -import { PrismaService } from "src/prisma/prisma.service"; +import { PrismaService } from "../prisma/prisma.service"; import { FeedbackDto } from "./dto/feedback.dto"; import { AddCourseDto } from "./dto/add-course.dto"; import { Course, CourseProgressStatus, CourseStatus, CourseVerificationStatus } from "@prisma/client"; diff --git a/src/mock-wallet/mock-wallet.module.ts b/src/mock-wallet/mock-wallet.module.ts index 0e0ca25..8d75d1d 100644 --- a/src/mock-wallet/mock-wallet.module.ts +++ b/src/mock-wallet/mock-wallet.module.ts @@ -4,6 +4,7 @@ import { MockWalletService } from './mock-wallet.service'; @Module({ controllers: [MockWalletController], - providers: [MockWalletService] + providers: [MockWalletService], + exports: [MockWalletService] }) export class MockWalletModule {} diff --git a/src/prisma/prisma.module.ts b/src/prisma/prisma.module.ts index efd8d91..c98d17c 100644 --- a/src/prisma/prisma.module.ts +++ b/src/prisma/prisma.module.ts @@ -3,6 +3,7 @@ import { PrismaService } from './prisma.service'; import { ConfigService } from '@nestjs/config'; @Module({ - providers: [PrismaService, ConfigService] + providers: [PrismaService, ConfigService], + exports: [PrismaService] }) export class PrismaModule {} From 5d8e1607769b00cb17d4f17b194af670fd7fa2bc Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Tue, 14 Nov 2023 14:49:57 +0530 Subject: [PATCH 21/88] debug --- src/admin/admin.controller.ts | 16 ++++----- src/admin/admin.service.ts | 16 ++++----- src/admin/dto/credit-request.dto.ts | 2 +- src/admin/dto/provider-settlement.dto.ts | 2 +- src/course/course.service.ts | 8 ++--- src/course/dto/course-response.dto.ts | 2 +- src/provider/dto/login.dto.ts | 2 +- .../dto/provider-profile-response.dto.ts | 2 +- src/provider/dto/signup.dto.ts | 2 +- src/provider/provider.controller.ts | 22 ++++++------ src/provider/provider.module.ts | 3 +- src/provider/provider.service.ts | 36 ++++++++++--------- 12 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index ec1ab5f..0700794 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Body, Get, Post, Patch, Res, Delete, HttpStatus, Param, ParseIntPipe, Logger} from '@nestjs/common'; +import { Controller, Body, Get, Post, Patch, Res, Delete, HttpStatus, Param, ParseIntPipe, Logger, ParseUUIDPipe} from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { AdminService } from './admin.service'; import { ProviderProfileResponse } from '../provider/dto/provider-profile-response.dto'; @@ -74,7 +74,7 @@ export class AdminController { @ApiOperation({ summary: "Settle credits for a provider" }) @ApiResponse({ status: HttpStatus.OK, type: json}) @Post('/:adminId/providers/settlements/settle') - async settleProvider(@Param("adminId", ParseIntPipe) adminId: number, @Body() settleDto: ProviderSettlementDto, @Res() res : Response) { + async settleProvider(@Param("adminId", ParseUUIDPipe) adminId: string, @Body() settleDto: ProviderSettlementDto, @Res() res : Response) { try { this.logger.log(`Settling the credits for the given provider`); @@ -100,7 +100,7 @@ export class AdminController { @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse}) @Get('/providers/:providerId') async getProviderProfile ( - @Param("providerId", ParseIntPipe) providerId: number, @Res() res: Response + @Param("providerId", ParseUUIDPipe) providerId: string, @Res() res: Response ) { try { this.logger.log(`Getting provider information for id ${providerId}`); @@ -129,7 +129,7 @@ export class AdminController { @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse}) @Patch('/providers/:providerId') async editProviderProfile ( - @Param("providerId", ParseIntPipe) providerId: number, @Body() providerDto: EditProvider ,@Res() res + @Param("providerId", ParseUUIDPipe) providerId: string, @Body() providerDto: EditProvider ,@Res() res ){ try { this.logger.log(`Getting provider information for id ${providerId}`); @@ -165,7 +165,7 @@ export class AdminController { @ApiOperation({ summary: "Verify provider" }) @ApiResponse({ status: HttpStatus.OK, type: json }) @Patch('/providers/:providerId/verify') - async verifyProvider(@Param("providerId", ParseIntPipe) providerId: number, @Res() res) { + async verifyProvider(@Param("providerId", ParseUUIDPipe) providerId: string, @Res() res) { try { this.logger.log(`Verifying the provider's account with id ${providerId}`); @@ -332,7 +332,7 @@ export class AdminController { @ApiOperation({ summary: "Get transaction history between admin and consumers"}) @ApiResponse({ status: HttpStatus.OK, type: TransactionResponse, isArray: true}) @Get('/:adminId/transactions/consumers') - async getTransactions (@Param("adminId") adminId: number, @Res() res + async getTransactions (@Param("adminId", ParseUUIDPipe) adminId: string, @Res() res ){ try { this.logger.log(`Getting all transactions between admin and consumers.`); @@ -358,7 +358,7 @@ export class AdminController { @ApiOperation({ summary: "Add credits to a Provider"}) @ApiResponse({ status: HttpStatus.OK, type: json}) @Post('/:adminId/providers/credits/addCredits') - async addCredits (@Param("adminId") adminId: number, @Body() reqBody: CreditRequest, @Res() res + async addCredits (@Param("adminId", ParseUUIDPipe) adminId: string, @Body() reqBody: CreditRequest, @Res() res ){ try { this.logger.log(`Adding credits to providers' wallet`); @@ -388,7 +388,7 @@ export class AdminController { @ApiOperation({ summary: "Remove credits from a Provider"}) @ApiResponse({ status: HttpStatus.OK, type: json }) @Post('/:adminId/providers/credits/reduceCredits') - async reduceCredits (@Param("adminId") adminId: number, @Body() reqBody: CreditRequest, @Res() res + async reduceCredits (@Param("adminId") adminId: string, @Body() reqBody: CreditRequest, @Res() res ){ try { this.logger.log(`Reducing credits from providers' wallet`); diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index fe4bb63..1826d3b 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -18,7 +18,7 @@ export class AdminService { private courseService: CourseService ) {} - async verifyProvider(providerId: number) { + async verifyProvider(providerId: string) { return this.providerService.verifyProvider(providerId); } @@ -28,7 +28,7 @@ export class AdminService { return this.providerService.fetchAllProviders(); } - async findProviderById(providerId: number): Promise { + async findProviderById(providerId: string): Promise { return this.providerService.getProvider(providerId); } @@ -58,7 +58,7 @@ export class AdminService { return this.courseService.removeCourse(courseId); } - async getTransactions(adminId: number) { + async getTransactions(adminId: string) { const walletService = process.env.WALLET_SERVICE_URL; const endpoint = `/admin/${adminId}/transactions/consumers`; @@ -72,7 +72,7 @@ export class AdminService { } } - async addOrRemoveCreditsToProvider(adminId: number, providerId: number, credits: number) { + async addOrRemoveCreditsToProvider(adminId: string, providerId: string, credits: number) { const walletService = process.env.WALLET_SERVICE_URL; let endpoint: string; if(credits >= 0) { @@ -98,7 +98,7 @@ export class AdminService { return this.providerService.editProviderProfileByAdmin(profileInfo); } - async getNoOfCoursePurchasesForProvider(providerId: number) { + async getNoOfCoursePurchasesForProvider(providerId: string) { return await this.prisma.userCourse.count({ where: { course: { @@ -108,7 +108,7 @@ export class AdminService { }); } - async getNumberOfCoursesForProvider(providerId: number) { + async getNumberOfCoursesForProvider(providerId: string) { return await this.prisma.course.count({ where: { providerId: providerId @@ -116,7 +116,7 @@ export class AdminService { }); } - async getProviderWalletCredits(providerId: number) { + async getProviderWalletCredits(providerId: string) { const url = process.env.WALLET_SERVICE_URL; const endpoint = url + `/api/providers/${providerId}/credits`; const resp = await axios.get(endpoint); @@ -138,7 +138,7 @@ export class AdminService { return results; } - async settleCredits(adminId: number, providerId: number) { + async settleCredits(adminId: string, providerId: string) { // Need to add transaction, add paymentReceipt additional settlement processing // then set the credits of the provider to 0 const totCredits = await this.getProviderWalletCredits(providerId); diff --git a/src/admin/dto/credit-request.dto.ts b/src/admin/dto/credit-request.dto.ts index 9eac012..32acf4f 100644 --- a/src/admin/dto/credit-request.dto.ts +++ b/src/admin/dto/credit-request.dto.ts @@ -6,7 +6,7 @@ export class CreditRequest { @ApiProperty() @IsNotEmpty() @IsNumber() - readonly providerId: number + readonly providerId: string @ApiProperty() @IsNotEmpty() diff --git a/src/admin/dto/provider-settlement.dto.ts b/src/admin/dto/provider-settlement.dto.ts index 1dae033..895e6c1 100644 --- a/src/admin/dto/provider-settlement.dto.ts +++ b/src/admin/dto/provider-settlement.dto.ts @@ -5,7 +5,7 @@ import { IsNumber, IsOptional, IsString } from 'class-validator'; export class ProviderSettlementDto { @ApiProperty({required: false}) @IsNumber() - id: number; + id: string; @ApiProperty() @IsOptional() diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 20caf81..c168fa2 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -28,7 +28,7 @@ export class CourseService { return courses; } - async addCourse(providerId: number, addCourseDto: AddCourseDto) { + async addCourse(providerId: string, addCourseDto: AddCourseDto) { return await this.prisma.course.create({ data: { @@ -56,7 +56,7 @@ export class CourseService { }); } - async archiveCourse(providerId: number, courseId: number) { + async archiveCourse(providerId: string, courseId: number) { return this.prisma.course.update({ where: { id: courseId }, @@ -65,7 +65,7 @@ export class CourseService { } - async editCourse(providerId: number, courseId: number, editCourseDto: EditCourseDto) { + async editCourse(providerId: string, courseId: number, editCourseDto: EditCourseDto) { return await this.prisma.course.update({ where: { id: courseId }, @@ -152,7 +152,7 @@ export class CourseService { }) } - async getProviderCourses(providerId: number) { + async getProviderCourses(providerId: string) { return this.prisma.course.findMany({ where: { diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index 9de456a..cd3f929 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -3,7 +3,7 @@ import { CourseStatus, CourseVerificationStatus } from "@prisma/client"; export class CourseResponse { readonly id: number; - readonly providerId: number; + readonly providerId: string; readonly title: string; readonly description: string; readonly courseLink: string; diff --git a/src/provider/dto/login.dto.ts b/src/provider/dto/login.dto.ts index 778658e..a77c470 100644 --- a/src/provider/dto/login.dto.ts +++ b/src/provider/dto/login.dto.ts @@ -19,5 +19,5 @@ export class LoginDto { export class LoginResponseDto { // provider ID - readonly providerId: number + readonly providerId: string } \ No newline at end of file diff --git a/src/provider/dto/provider-profile-response.dto.ts b/src/provider/dto/provider-profile-response.dto.ts index 07bd10e..b22c2ef 100644 --- a/src/provider/dto/provider-profile-response.dto.ts +++ b/src/provider/dto/provider-profile-response.dto.ts @@ -7,7 +7,7 @@ export class ProviderProfileResponse { @ApiProperty({required: false}) @IsNumber() @IsOptional() - id: number; + id: string; @ApiProperty() @IsOptional() diff --git a/src/provider/dto/signup.dto.ts b/src/provider/dto/signup.dto.ts index cc26522..4b51e71 100644 --- a/src/provider/dto/signup.dto.ts +++ b/src/provider/dto/signup.dto.ts @@ -30,5 +30,5 @@ export class SignupDto { export class SignupResponseDto { // provider ID - readonly providerId: number + readonly providerId: string } \ No newline at end of file diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index f280641..91a8b77 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, HttpStatus, Param, ParseIntPipe, Patch, Post, Put, Res } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpStatus, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Put, Res } from '@nestjs/common'; import { ProviderService } from './provider.service'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SignupDto, SignupResponseDto } from './dto/signup.dto'; @@ -60,7 +60,7 @@ export class ProviderController { @Get("/:providerId/profile") // view provider profile information async viewProfile( - @Param("providerId", ParseIntPipe) providerId: number, + @Param("providerId", ParseUUIDPipe) providerId: string, @Res() res ) { const provider = await this.providerService.getProvider(providerId); @@ -76,7 +76,7 @@ export class ProviderController { @Put("/:providerId/profile") // update provider profile information async updateProfile( - @Param("providerId", ParseIntPipe) providerId: number, + @Param("providerId", ParseUUIDPipe) providerId: string, @Body() updateProfileDto: UpdateProfileDto, @Res() res ) { @@ -92,7 +92,7 @@ export class ProviderController { @Patch("/:providerId/course/:courseId") // edit course information async editCourse( - @Param("providerId", ParseIntPipe) providerId: number, + @Param("providerId", ParseUUIDPipe) providerId: string, @Param("courseId", ParseIntPipe) courseId: number, @Body() editCourseDto: EditCourseDto, @Res() res @@ -109,7 +109,7 @@ export class ProviderController { @Get("/:providerId/course/:courseId/archive") // edit course information async archiveCourse( - @Param("providerId", ParseIntPipe) providerId: number, + @Param("providerId", ParseUUIDPipe) providerId: string, @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { @@ -125,7 +125,7 @@ export class ProviderController { @Post("/:providerId/course") // add new course async addCourse( - @Param("providerId", ParseIntPipe) providerId: number, + @Param("providerId", ParseUUIDPipe) providerId: string, @Body() addCourseDto: AddCourseDto, @Res() res ) { @@ -142,7 +142,7 @@ export class ProviderController { @Delete("/:providerId/course/:courseId") // remove an existing course async removeCourse( - @Param("providerId", ParseIntPipe) providerId: number, + @Param("providerId", ParseUUIDPipe) providerId: string, @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { @@ -158,7 +158,7 @@ export class ProviderController { @Get("/:providerId/course") // View courses offered by self async fetchProviderCourses( - @Param("providerId", ParseIntPipe) providerId: number, + @Param("providerId", ParseUUIDPipe) providerId: string, @Res() res ) { const courses = await this.providerService.getCourses(providerId); @@ -174,7 +174,7 @@ export class ProviderController { @Get("/:providerId/course/:courseId/feedback") // View Course Feedback & ratings, numberOfPurchases async getCourseFeedback( - @Param("providerId", ParseIntPipe) providerId: number, + @Param("providerId", ParseUUIDPipe) providerId: string, @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { @@ -191,7 +191,7 @@ export class ProviderController { @Get("/:providerId/course/:courseId/purchases") // Get all the transactions for course purchase user wise async getCoursePurchases( - @Param("providerId", ParseIntPipe) providerId: number, + @Param("providerId", ParseUUIDPipe) providerId: string, @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { @@ -208,7 +208,7 @@ export class ProviderController { @Patch("/:providerId/course/completion") // Mark course as complete for a user async markCourseComplete( - @Param("providerId", ParseIntPipe) providerId: number, + @Param("providerId", ParseUUIDPipe) providerId: string, @Body() completeCourseDto: CompleteCourseDto, @Res() res ) { diff --git a/src/provider/provider.module.ts b/src/provider/provider.module.ts index 7431959..7d9ee4d 100644 --- a/src/provider/provider.module.ts +++ b/src/provider/provider.module.ts @@ -8,6 +8,7 @@ import { PrismaService } from 'src/prisma/prisma.service'; @Module({ imports: [PrismaModule], controllers: [ProviderController], - providers: [ProviderService, CourseService, PrismaService] + providers: [ProviderService, CourseService, PrismaService], + exports: [ProviderService] }) export class ProviderModule {} \ No newline at end of file diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 6a4779b..fba49f5 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, HttpException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { SignupDto } from './dto/signup.dto'; import { PrismaService } from 'src/prisma/prisma.service'; -import { Prisma, Provider, ProviderStatus, WalletType } from '@prisma/client'; +import { Prisma, Provider, ProviderStatus } from '@prisma/client'; import { LoginDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { AddCourseDto } from 'src/course/dto/add-course.dto'; @@ -13,6 +13,7 @@ import { EditCourseDto } from 'src/course/dto/edit-course.dto'; import { EditProvider } from 'src/admin/dto/edit-provider.dto'; import { CourseResponse } from 'src/course/dto/course-response.dto'; import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; +import axios from 'axios'; @Injectable() export class ProviderService { @@ -31,14 +32,15 @@ export class ProviderService { if(provider) throw new BadRequestException("Account with that email ID already exists"); + // update the endpoint + const url = process.env.WALLET_SERVICE_URL; + const endpoint = url + `/admin/:adminId/providers/create`; + const resp = await axios.post(endpoint); + provider = await this.prisma.provider.create({ data: { ...signupDto, - wallet: { - create: { - type: WalletType.provider, - } - } + walletId: resp.data.walletId } }) return provider.id @@ -60,7 +62,7 @@ export class ProviderService { return provider.id } - async getProvider(providerId: number) { + async getProvider(providerId: string) { const provider = await this.prisma.provider.findUnique({ where: { @@ -73,7 +75,7 @@ export class ProviderService { return provider; } - async updateProfileInfo(providerId: number, updateProfileDto: UpdateProfileDto) { + async updateProfileInfo(providerId: string, updateProfileDto: UpdateProfileDto) { try { await this.prisma.provider.update({ @@ -95,7 +97,7 @@ export class ProviderService { }); } - async addNewCourse(providerId: number, addCourseDto: AddCourseDto) { + async addNewCourse(providerId: string, addCourseDto: AddCourseDto) { const provider = await this.getProvider(providerId); @@ -105,7 +107,7 @@ export class ProviderService { return this.courseService.addCourse(providerId, addCourseDto); } - async removeCourse(providerId: number, courseId: number) { + async removeCourse(providerId: string, courseId: number) { const course = await this.courseService.getCourse(courseId); if(!course) @@ -117,21 +119,21 @@ export class ProviderService { await this.courseService.deleteCourse(courseId); } - async getCourses(providerId: number): Promise { + async getCourses(providerId: string): Promise { return this.courseService.getProviderCourses(providerId); } - async editCourse(providerId: number, courseId: number, editCourseDto: EditCourseDto) { + async editCourse(providerId: string, courseId: number, editCourseDto: EditCourseDto) { const course = await this.courseService.editCourse(providerId, courseId, editCourseDto); return course; } - async archiveCourse(providerId: number, courseId: number) { + async archiveCourse(providerId: string, courseId: number) { return this.courseService.archiveCourse(providerId, courseId); } - async getCourseFeedbacks(providerId: number, courseId: number): Promise { + async getCourseFeedbacks(providerId: string, courseId: number): Promise { const course = await this.courseService.getCourse(courseId); @@ -155,7 +157,7 @@ export class ProviderService { }; } - async getCoursePurchases(providerId: number, courseId: number): Promise { + async getCoursePurchases(providerId: string, courseId: number): Promise { const course = await this.courseService.getCourse(courseId); @@ -174,7 +176,7 @@ export class ProviderService { } - async markCourseComplete(providerId: number, completeCourseDto: CompleteCourseDto) { + async markCourseComplete(providerId: string, completeCourseDto: CompleteCourseDto) { const course = await this.courseService.getCourse(completeCourseDto.courseId); if(!course) @@ -197,7 +199,7 @@ export class ProviderService { }); } - async verifyProvider(providerId: number) { + async verifyProvider(providerId: string) { let providerInfo = await this.getProvider(providerId); if(providerInfo.status != ProviderStatus.pending) { From 791221966a85bea312e332506a361533ff6f9b8e Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Tue, 14 Nov 2023 15:52:50 +0530 Subject: [PATCH 22/88] fixed enums --- prisma/schema.prisma | 43 +++++++++++-------- prisma/seed.ts | 32 ++++++-------- src/admin/admin.controller.ts | 1 - src/admin/admin.service.ts | 1 + src/course/course.service.ts | 14 +++--- .../dto/provider-profile-response.dto.ts | 4 -- src/provider/provider.service.ts | 27 ++++++------ 7 files changed, 61 insertions(+), 61 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 947f121..c63eaaa 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,26 +13,32 @@ datasource db { // Here goes models enum ProviderStatus { - pending - verified - rejected + PENDING + VERIFIED + REJECTED +} + +enum WalletType { + ADMIN + PROVIDER + CONSUMER } enum CourseVerificationStatus { - pending - accepted - rejected + PENDING + ACCEPTED + REJECTED } enum CourseStatus { - active - inactive - archived + ACTIVE + INACTIVE + ARCHIVED } enum CourseProgressStatus { - inProgress - completed + IN_PROGRESS + COMPLETED } model Admin { @@ -48,9 +54,8 @@ model Provider { name String email String @unique password String - walletId Int @unique paymentInfo Json? - status ProviderStatus @default(pending) + status ProviderStatus @default(PENDING) courses Course[] } @@ -71,7 +76,7 @@ model Course { avgRating Float? status CourseStatus availabilityTime DateTime? - verificationStatus CourseVerificationStatus @default(pending) + verificationStatus CourseVerificationStatus @default(PENDING) cqfScore Int? impactScore Float? provider Provider @relation(fields: [providerId], references: [id]) @@ -83,7 +88,7 @@ model UserCourse { userId String @db.Uuid courseId Int purchasedAt DateTime @default(now()) - status CourseProgressStatus @default(inProgress) + status CourseProgressStatus @default(IN_PROGRESS) courseCompletionScore Float? rating Int? feedback String? @@ -93,7 +98,7 @@ model UserCourse { } enum TransactionType { - purchase - creditRequest - settlement -} \ No newline at end of file + PURCHASE + CREDIT_REQUEST + SETTLEMENT +} diff --git a/prisma/seed.ts b/prisma/seed.ts index 2dc4730..28d9705 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -8,8 +8,7 @@ async function main() { name: "Vijay Salgaonkar", email: "vijaysalgaonkar@gmail.com", password: "9d209aacaed4088d68c41bd8dfb20de39cbd8339", - walletId: 1, - status: ProviderStatus.verified + status: ProviderStatus.VERIFIED } }); @@ -18,14 +17,13 @@ async function main() { name: "udemy", email: "udemyorg@gmail.in", password: "Udemy@9812", - walletId: 2, paymentInfo: { bankAccNo: "1111111111", otherDetails: { } }, - status: 'verified', + status: 'VERIFIED', // courses: [] } }); @@ -35,14 +33,13 @@ async function main() { name: "coursera", email: "coursera@gmail.in", password: "Coursera@999", - walletId: 3, paymentInfo: { bankAccNo: "1111111113", otherDetails: { } }, - status: 'pending', + status: 'PENDING', // courses: [] } }); @@ -52,14 +49,13 @@ async function main() { name: "lern", email: "lern@gmail.in", password: "lern@999", - walletId: 4, paymentInfo: { bankAccNo: "1111111116", otherDetails: { } }, - status: 'rejected', + status: 'REJECTED', // courses: [] } }); @@ -81,7 +77,7 @@ async function main() { "Backend engineering": ["Level1"] }, author: "Stephen Grider", - status: "active", + status: "ACTIVE", availabilityTime: new Date("2024-05-01").toISOString() }, { providerId: provider1.id, @@ -98,7 +94,7 @@ async function main() { "Understanding brand": ["Level1"] }, author: "Lindsay Marsh", - status: "active", + status: "ACTIVE", availabilityTime: new Date("2024-05-01").toISOString() }, { providerId: provider1.id, @@ -116,7 +112,7 @@ async function main() { "MySQL": ["Level1"] }, author: "Jose Portilla", - status: "active", + status: "ACTIVE", availabilityTime: new Date("2024-05-01").toISOString() }, { providerId: response.id, @@ -132,7 +128,7 @@ async function main() { "Excel": ["Level1", "Level2", "Level3", "Level4"] }, author: "Kyle Pew", - status: "active", + status: "ACTIVE", availabilityTime: new Date("2024-05-01").toISOString() }] }) @@ -183,9 +179,9 @@ async function main() { "Orchestration": [ "Level5" ] }, author: "Jason Frig", - status: CourseStatus.active, + status: CourseStatus.ACTIVE, availabilityTime: new Date("2023-06-01"), - verificationStatus: CourseVerificationStatus.accepted, + verificationStatus: CourseVerificationStatus.ACCEPTED, cqfScore: 10, } }); @@ -206,9 +202,9 @@ async function main() { "Python": [ "Level1", "Level2" ] }, author: "James Franco", - status: CourseStatus.active, + status: CourseStatus.ACTIVE, availabilityTime: new Date("2023-08-10"), - verificationStatus: CourseVerificationStatus.pending, + verificationStatus: CourseVerificationStatus.PENDING, } }); @@ -228,9 +224,9 @@ async function main() { "LLVM": [ "Level4" ] }, author: "Ramakrishna Upadrasta", - status: CourseStatus.active, + status: CourseStatus.ACTIVE, availabilityTime: new Date("2023-10-10"), - verificationStatus: CourseVerificationStatus.rejected, + verificationStatus: CourseVerificationStatus.REJECTED, } }); diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 0700794..4c67578 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -139,7 +139,6 @@ export class AdminController { name: providerDto.name, email: providerDto.email, password: providerDto.password, - walletId: providerDto.walletId, status: providerDto.status } diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 1826d3b..01bb9b4 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -120,6 +120,7 @@ export class AdminService { const url = process.env.WALLET_SERVICE_URL; const endpoint = url + `/api/providers/${providerId}/credits`; const resp = await axios.get(endpoint); + return resp.data.credits; } async getAllProviderInfoForSettlement() { diff --git a/src/course/course.service.ts b/src/course/course.service.ts index c168fa2..2714fe5 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -60,7 +60,7 @@ export class CourseService { return this.prisma.course.update({ where: { id: courseId }, - data: { status: CourseStatus.archived } + data: { status: CourseStatus.ARCHIVED } }); } @@ -81,7 +81,7 @@ export class CourseService { duration: editCourseDto.duration, competency: editCourseDto.competency, author: editCourseDto.author, - verificationStatus: CourseVerificationStatus.pending, + verificationStatus: CourseVerificationStatus.PENDING, availabilityTime: editCourseDto.availabilityTime } }) @@ -127,7 +127,7 @@ export class CourseService { if(!userCourse) throw new NotFoundException("This user has not subscribed to this course"); - if(userCourse.status != CourseProgressStatus.completed) + if(userCourse.status != CourseProgressStatus.COMPLETED) throw new BadRequestException("Course not complete"); await this.prisma.userCourse.update({ @@ -180,7 +180,7 @@ export class CourseService { } }, data: { - status: CourseProgressStatus.completed + status: CourseProgressStatus.COMPLETED } }) } @@ -194,13 +194,13 @@ export class CourseService { let course = await this.getCourse(courseId); - if(course.verificationStatus != CourseVerificationStatus.pending) { + if(course.verificationStatus != CourseVerificationStatus.PENDING) { throw new HttpException(`Course is either rejected or is already accepted.`, 406); } return this.prisma.course.update({ where: { id: courseId }, data: { - verificationStatus: CourseVerificationStatus.accepted, + verificationStatus: CourseVerificationStatus.ACCEPTED, cqfScore: cqf_score } }); @@ -212,7 +212,7 @@ export class CourseService { return this.prisma.course.update({ where: {id: courseId}, - data: {verificationStatus: CourseVerificationStatus.rejected} + data: {verificationStatus: CourseVerificationStatus.REJECTED} }); } diff --git a/src/provider/dto/provider-profile-response.dto.ts b/src/provider/dto/provider-profile-response.dto.ts index b22c2ef..d41c0df 100644 --- a/src/provider/dto/provider-profile-response.dto.ts +++ b/src/provider/dto/provider-profile-response.dto.ts @@ -24,10 +24,6 @@ export class ProviderProfileResponse { @IsOptional() password?: string; - @ApiProperty() - @IsNumber() - @IsOptional() - walletId: number; @ApiProperty() @IsOptional() diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index fba49f5..f912fc7 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, HttpException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { SignupDto } from './dto/signup.dto'; import { PrismaService } from 'src/prisma/prisma.service'; -import { Prisma, Provider, ProviderStatus } from '@prisma/client'; +import { ProviderStatus } from '@prisma/client'; import { LoginDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { AddCourseDto } from 'src/course/dto/add-course.dto'; @@ -32,17 +32,20 @@ export class ProviderService { if(provider) throw new BadRequestException("Account with that email ID already exists"); - // update the endpoint - const url = process.env.WALLET_SERVICE_URL; - const endpoint = url + `/admin/:adminId/providers/create`; - const resp = await axios.post(endpoint); - provider = await this.prisma.provider.create({ data: { ...signupDto, - walletId: resp.data.walletId } - }) + }); + + const url = process.env.WALLET_SERVICE_URL; + const endpoint = url + `/api/wallet/create`; + const reqBody = { + userId: provider.id, + type: 'PROVIDER' + } + const resp = await axios.post(endpoint, reqBody); + return provider.id } @@ -101,7 +104,7 @@ export class ProviderService { const provider = await this.getProvider(providerId); - if(provider.status != ProviderStatus.verified) + if(provider.status != ProviderStatus.VERIFIED) throw new UnauthorizedException("Provider account is not verified"); return this.courseService.addCourse(providerId, addCourseDto); @@ -195,19 +198,19 @@ export class ProviderService { async fetchAllProviders(): Promise { return this.prisma.provider.findMany({ - select: { id: true, name: true, email: true, walletId: true, paymentInfo: true, courses: true, status: true} + select: { id: true, name: true, email: true, paymentInfo: true, courses: true, status: true} }); } async verifyProvider(providerId: string) { let providerInfo = await this.getProvider(providerId); - if(providerInfo.status != ProviderStatus.pending) { + if(providerInfo.status != ProviderStatus.PENDING) { throw new HttpException(`Provider is either verified or rejected.`, 406); } return this.prisma.provider.update({ where: {id: providerId}, - data: {status: ProviderStatus.verified} + data: {status: ProviderStatus.VERIFIED} }); } } \ No newline at end of file From 232ab59292711d7e8ea04931fef0c11e5f546d5f Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Tue, 14 Nov 2023 17:42:12 +0530 Subject: [PATCH 23/88] logger and error handling --- src/admin/admin.module.ts | 3 +- src/course/course.controller.ts | 98 +++++++-- src/course/dto/edit-course.dto.ts | 19 +- src/provider/provider.controller.ts | 301 ++++++++++++++++++++++------ src/provider/provider.module.ts | 3 +- 5 files changed, 329 insertions(+), 95 deletions(-) diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts index 3c1c647..78bdeb5 100644 --- a/src/admin/admin.module.ts +++ b/src/admin/admin.module.ts @@ -10,6 +10,7 @@ import { ProviderModule } from 'src/provider/provider.module'; @Module({ imports: [PrismaModule, CourseModule, MockWalletModule, ProviderModule], controllers: [AdminController], - providers: [AdminService, PrismaService] + providers: [AdminService, PrismaService], + exports: [AdminService] }) export class AdminModule {} diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index 4d4bd96..0e829ad 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -1,12 +1,16 @@ -import { Body, Controller, Get, HttpStatus, Param, ParseIntPipe, Patch, Post, Res } from "@nestjs/common"; +import { Body, Controller, Get, HttpStatus, Logger, Param, ParseIntPipe, Patch, Post, Res } from "@nestjs/common"; import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { CourseService } from "./course.service"; import { FeedbackDto } from "./dto/feedback.dto"; import { CourseResponse } from "./dto/course-response.dto"; +import { getPrismaErrorStatusAndMessage } from "src/utils/utils"; @Controller('course') @ApiTags('course') export class CourseController { + + private readonly logger = new Logger(CourseController.name); + constructor(private readonly courseService: CourseService) {} @ApiOperation({ summary: 'Search courses' }) @@ -17,12 +21,26 @@ export class CourseController { // @Body() searchDto: SearchDto, @Res() res ) { - const courses = await this.courseService.searchCourses(); + try { + this.logger.log(`Getting information of courses`); + + const courses = await this.courseService.searchCourses(); + + this.logger.log(`Successfully retrieved the courses`); - res.status(HttpStatus.OK).json({ - message: "search successful", - data: courses - }) + res.status(HttpStatus.OK).json({ + message: "search successful", + data: courses + }) + } catch (err) { + this.logger.error(`Failed to retreive the courses' information`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the courses' information", + }); + } } @ApiOperation({ summary: 'Fetch details of one course' }) @@ -33,12 +51,26 @@ export class CourseController { @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { - const course: CourseResponse = await this.courseService.getCourse(courseId); + try { + this.logger.log(`Getting information of one course`); + + const course: CourseResponse = await this.courseService.getCourse(courseId); + + this.logger.log(`Successfully retrieved the course`); - res.status(HttpStatus.OK).json({ - message: "fetch successful", - data: course - }) + res.status(HttpStatus.OK).json({ + message: "fetch successful", + data: course + }) + } catch (err) { + this.logger.error(`Failed to retreive the course`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the course", + }); + } } @ApiOperation({ summary: 'Confirmation of user purchase of a course' }) @@ -50,11 +82,25 @@ export class CourseController { @Param("userId") userId: string, @Res() res ) { - await this.courseService.insertUserCourse(courseId, userId); + try { + this.logger.log(`Recording the user purchase of the course`); + + await this.courseService.insertUserCourse(courseId, userId); + + this.logger.log(`Successfully recorded the purchase`); + + res.status(HttpStatus.OK).json({ + message: "purchase successful" + }) + } catch (err) { + this.logger.error(`Failed to record the purchase`); - res.status(HttpStatus.OK).json({ - message: "purchase successful" - }) + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to record the purchase", + }); + } } @ApiOperation({ summary: 'Give feedback and rating' }) @@ -67,11 +113,25 @@ export class CourseController { @Body() feedbackDto: FeedbackDto, @Res() res ) { - await this.courseService.giveCourseFeedback(courseId, userId, feedbackDto); + try { + this.logger.log(`Recording the course feedback`); + + await this.courseService.giveCourseFeedback(courseId, userId, feedbackDto); + + this.logger.log(`Successfully recorded the feedback`); + + res.status(HttpStatus.OK).json({ + message: "feedback successful" + }) + } catch (err) { + this.logger.error(`Failed to record the feedback`); - res.status(HttpStatus.OK).json({ - message: "feedback successful" - }) + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to record the feedback", + }); + } } } diff --git a/src/course/dto/edit-course.dto.ts b/src/course/dto/edit-course.dto.ts index af01794..b88d9db 100644 --- a/src/course/dto/edit-course.dto.ts +++ b/src/course/dto/edit-course.dto.ts @@ -9,32 +9,32 @@ export class EditCourseDto { @ApiProperty() @IsString() @IsOptional() - title: string; + title?: string; // description @ApiProperty() @IsString() @IsOptional() - description: string; + description?: string; // link for the course content @ApiProperty() @IsString() @IsOptional() - courseLink: string; + courseLink?: string; // course image @ApiProperty() @IsString() @IsOptional() - imgLink: string; + imgLink?: string; // number of credits required to purchase course @ApiProperty() @Min(0) @IsInt() @IsOptional() - credits: number; + credits?: number; // Number of lessons @ApiProperty() @@ -46,25 +46,25 @@ export class EditCourseDto { @ApiProperty() @IsArray() @IsOptional() - language: string[]; + language?: string[]; // course duration @ApiProperty() @Min(0) @IsInt() @IsOptional() - duration: number; + duration?: number; // competency @ApiProperty() @IsOptional() - competency: any; + competency?: any; // author @ApiProperty() @IsString() @IsOptional() - author: string; + author?: string; // course status (active/inactive/archived) // @ApiProperty() @@ -77,5 +77,4 @@ export class EditCourseDto { @IsDate() @IsOptional() availabilityTime?: Date; - } \ No newline at end of file diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index f280641..f0e18c8 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, HttpStatus, Param, ParseIntPipe, Patch, Post, Put, Res } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, ParseIntPipe, Patch, Post, Put, Res } from '@nestjs/common'; import { ProviderService } from './provider.service'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SignupDto, SignupResponseDto } from './dto/signup.dto'; @@ -11,10 +11,14 @@ import { CompleteCourseDto } from 'src/course/dto/completion.dto'; import { EditCourseDto } from 'src/course/dto/edit-course.dto'; import { CourseResponse } from 'src/course/dto/course-response.dto'; import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; +import { getPrismaErrorStatusAndMessage } from 'src/utils/utils'; @Controller('provider') @ApiTags('provider') export class ProviderController { + + private readonly logger = new Logger(ProviderController.name); + constructor( private providerService: ProviderService, ) {} @@ -27,14 +31,28 @@ export class ProviderController { @Body() signupDto: SignupDto, @Res() res ) { - const providerId = await this.providerService.createNewAccount(signupDto); - - res.status(HttpStatus.CREATED).json({ - message: "account created successfully", - data: { - providerId - } - }) + try { + this.logger.log(`Creating new provider account`); + + const providerId = await this.providerService.createNewAccount(signupDto); + + this.logger.log(`successfully created new provider account`); + + res.status(HttpStatus.CREATED).json({ + message: "account created successfully", + data: { + providerId + } + }) + } catch (err) { + this.logger.error(`Failed to create new provider account`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to create new provider account`", + }); + } } @ApiOperation({ summary: 'provider login' }) @@ -45,14 +63,28 @@ export class ProviderController { @Body() loginDto: LoginDto, @Res() res ) { - const providerId = await this.providerService.getProviderIdFromLogin(loginDto); - - res.status(HttpStatus.OK).json({ - message: "login successful", - data: { - providerId - } - }) + try { + this.logger.log(`Getting provider ID`); + + const providerId = await this.providerService.getProviderIdFromLogin(loginDto); + + this.logger.log(`successfully logged in`); + + res.status(HttpStatus.OK).json({ + message: "login successful", + data: { + providerId + } + }) + } catch (err) { + this.logger.error(`Failed to log in`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to log in`", + }); + } } @ApiOperation({ summary: 'view provider profile' }) @@ -63,12 +95,26 @@ export class ProviderController { @Param("providerId", ParseIntPipe) providerId: number, @Res() res ) { - const provider = await this.providerService.getProvider(providerId); + try { + this.logger.log(`Getting provider profile`); - res.status(HttpStatus.OK).json({ - message: "fetch successful", - data : provider - }) + const provider = await this.providerService.getProvider(providerId); + + this.logger.log(`successfully retreived provider profile`); + + res.status(HttpStatus.OK).json({ + message: "fetch successful", + data : provider + }) + } catch (err) { + this.logger.error(`Failed to retreive provider profile`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to retreive provider profile`", + }); + } } @ApiOperation({ summary: 'update provider profile information' }) @@ -80,11 +126,25 @@ export class ProviderController { @Body() updateProfileDto: UpdateProfileDto, @Res() res ) { - await this.providerService.updateProfileInfo(providerId, updateProfileDto); + try { + this.logger.log(`Updating provider profile`); + + await this.providerService.updateProfileInfo(providerId, updateProfileDto); + + this.logger.log(`successfully updated provider profile`); + + res.status(HttpStatus.OK).json({ + message: "account updated successfully", + }) + } catch (err) { + this.logger.error(`Failed to update provider profile`); - res.status(HttpStatus.OK).json({ - message: "account updated successfully", - }) + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to update provider profile`", + }); + } } @ApiOperation({ summary: 'edit course information' }) @@ -97,27 +157,55 @@ export class ProviderController { @Body() editCourseDto: EditCourseDto, @Res() res ) { - await this.providerService.editCourse(providerId, courseId, editCourseDto); + try { + this.logger.log(`Updating course information`); - res.status(HttpStatus.OK).json({ - message: "course edited successfully", - }) + await this.providerService.editCourse(providerId, courseId, editCourseDto); + + this.logger.log(`Successfully updated course information`); + + res.status(HttpStatus.OK).json({ + message: "course edited successfully", + }) + } catch (err) { + this.logger.error(`Failed to update course information`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to update course information`", + }); + } } - @ApiOperation({ summary: 'Archive course ' }) + @ApiOperation({ summary: 'Archive course' }) @ApiResponse({ status: HttpStatus.OK }) - @Get("/:providerId/course/:courseId/archive") + @Patch("/:providerId/course/:courseId/archive") // edit course information async archiveCourse( @Param("providerId", ParseIntPipe) providerId: number, @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { - await this.providerService.archiveCourse(providerId, courseId); + try { + this.logger.log(`Archiving course`); + + await this.providerService.archiveCourse(providerId, courseId); + + this.logger.log(`Successfully archived the course`); - res.status(HttpStatus.OK).json({ - message: "course edited successfully", - }) + res.status(HttpStatus.OK).json({ + message: "course archived successfully", + }) + } catch (err) { + this.logger.error(`Failed to archive the course`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to archive the course`", + }); + } } @ApiOperation({ summary: 'add new course' }) @@ -129,12 +217,26 @@ export class ProviderController { @Body() addCourseDto: AddCourseDto, @Res() res ) { - const course = await this.providerService.addNewCourse(providerId, addCourseDto); + try { + this.logger.log(`Adding new course`); + + const course = await this.providerService.addNewCourse(providerId, addCourseDto); + + this.logger.log(`Successfully added new course`); - res.status(HttpStatus.CREATED).json({ - message: "course added successfully", - data: course - }) + res.status(HttpStatus.CREATED).json({ + message: "course added successfully", + data: course + }) + } catch (err) { + this.logger.error(`Failed to add the course`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to add the course`", + }); + } } @ApiOperation({ summary: 'remove a course' }) @@ -146,11 +248,26 @@ export class ProviderController { @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { - await this.providerService.removeCourse(providerId, courseId); + try { + this.logger.log(`Removing course`); + + await this.providerService.removeCourse(providerId, courseId); - res.status(HttpStatus.OK).json({ - message: "course deleted successfully", - }) + this.logger.log(`Successfully deleted the course`); + + + res.status(HttpStatus.OK).json({ + message: "course deleted successfully", + }) + } catch (err) { + this.logger.error(`Failed to delete the course`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to delete the course`", + }); + } } @ApiOperation({ summary: 'View courses offered by self' }) @@ -161,12 +278,26 @@ export class ProviderController { @Param("providerId", ParseIntPipe) providerId: number, @Res() res ) { - const courses = await this.providerService.getCourses(providerId); + try { + this.logger.log(`Getting courses`); + + const courses = await this.providerService.getCourses(providerId); + + this.logger.log(`Successfully retrieved the courses`); - res.status(HttpStatus.OK).json({ - message: "courses fetched successfully", - data: courses - }) + res.status(HttpStatus.OK).json({ + message: "courses fetched successfully", + data: courses + }) + } catch (err) { + this.logger.error(`Failed to fetch the courses`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the courses", + }); + } } @ApiOperation({ summary: 'View Course Feedback & ratings, numberOfPurchases' }) @@ -178,12 +309,26 @@ export class ProviderController { @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { - const feedbackResponse = await this.providerService.getCourseFeedbacks(providerId, courseId); + try { + this.logger.log(`Getting course feedbacks`); + + const feedbackResponse = await this.providerService.getCourseFeedbacks(providerId, courseId); + + this.logger.log(`Successfully retrieved the feedbacks`); - res.status(HttpStatus.OK).json({ - message: "feedbacks fetched successfully", - data: feedbackResponse - }) + res.status(HttpStatus.OK).json({ + message: "feedbacks fetched successfully", + data: feedbackResponse + }) + } catch (err) { + this.logger.error(`Failed to fetch the feedbacks`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the feedbacks", + }); + } } @ApiOperation({ summary: 'Get all transactions for course purchase user wise' }) @@ -195,12 +340,26 @@ export class ProviderController { @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { - const purchaseResponse = await this.providerService.getCoursePurchases(providerId, courseId); + try { + this.logger.log(`Getting course purchase transactions`); + + const purchaseResponse = await this.providerService.getCoursePurchases(providerId, courseId); - res.status(HttpStatus.OK).json({ - message: "purchases fetched successfully", - data: purchaseResponse - }) + this.logger.log(`Successfully retrieved course purchase transactions`); + + res.status(HttpStatus.OK).json({ + message: "purchases fetched successfully", + data: purchaseResponse + }) + } catch (err) { + this.logger.error(`Failed to fetch the transactions`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the transactions", + }); + } } @ApiOperation({ summary: 'Mark course as complete' }) @@ -212,10 +371,24 @@ export class ProviderController { @Body() completeCourseDto: CompleteCourseDto, @Res() res ) { - await this.providerService.markCourseComplete(providerId, completeCourseDto); + try { + this.logger.log(`Updating course as complete`); + + await this.providerService.markCourseComplete(providerId, completeCourseDto); + + this.logger.log(`Successfully marked the course as complete`); + + res.status(HttpStatus.OK).json({ + message: "course marked complete", + }) + } catch (err) { + this.logger.error(`Failed to mark the course completion`); - res.status(HttpStatus.OK).json({ - message: "course marked complete", - }) + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to mark the course completion", + }); + } } } \ No newline at end of file diff --git a/src/provider/provider.module.ts b/src/provider/provider.module.ts index 7431959..7d9ee4d 100644 --- a/src/provider/provider.module.ts +++ b/src/provider/provider.module.ts @@ -8,6 +8,7 @@ import { PrismaService } from 'src/prisma/prisma.service'; @Module({ imports: [PrismaModule], controllers: [ProviderController], - providers: [ProviderService, CourseService, PrismaService] + providers: [ProviderService, CourseService, PrismaService], + exports: [ProviderService] }) export class ProviderModule {} \ No newline at end of file From 279c046d36d775216d0959a58551ec428ea33210 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Tue, 14 Nov 2023 19:03:16 +0530 Subject: [PATCH 24/88] review bug fixes --- src/course/course.controller.ts | 6 +++--- src/course/course.service.ts | 21 +++++---------------- src/course/dto/add-course.dto.ts | 11 ++++++----- src/course/dto/completion.dto.ts | 4 ++-- src/course/dto/course-response.dto.ts | 4 +++- src/course/dto/edit-course.dto.ts | 3 ++- src/course/dto/search.dto.ts | 5 +++-- src/provider/dto/login.dto.ts | 6 +++--- src/provider/dto/signup.dto.ts | 16 +++++++++++++--- src/provider/provider.service.ts | 8 ++++---- src/utils/types.ts | 2 ++ 11 files changed, 46 insertions(+), 40 deletions(-) create mode 100644 src/utils/types.ts diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index 0e829ad..c3b21a6 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, HttpStatus, Logger, Param, ParseIntPipe, Patch, Post, Res } from "@nestjs/common"; +import { Body, Controller, Get, HttpStatus, Logger, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Res } from "@nestjs/common"; import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { CourseService } from "./course.service"; import { FeedbackDto } from "./dto/feedback.dto"; @@ -79,7 +79,7 @@ export class CourseController { // Confirmation of user purchase of a course async purchaseCourse( @Param("courseId", ParseIntPipe) courseId: number, - @Param("userId") userId: string, + @Param("userId", ParseUUIDPipe) userId: string, @Res() res ) { try { @@ -109,7 +109,7 @@ export class CourseController { // Give feedback and rating async feedback( @Param("courseId", ParseIntPipe) courseId: number, - @Param("userId") userId: string, + @Param("userId", ParseUUIDPipe) userId: string, @Body() feedbackDto: FeedbackDto, @Res() res ) { diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 2714fe5..7f536da 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -1,4 +1,4 @@ -import { NotFoundException, BadRequestException, Injectable, HttpException } from "@nestjs/common"; +import { NotFoundException, BadRequestException, Injectable, NotAcceptableException } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import { FeedbackDto } from "./dto/feedback.dto"; import { AddCourseDto } from "./dto/add-course.dto"; @@ -70,21 +70,10 @@ export class CourseService { return await this.prisma.course.update({ where: { id: courseId }, data: { - providerId, - title: editCourseDto.title, - description: editCourseDto.description, - courseLink: editCourseDto.courseLink, - imgLink: editCourseDto.imgLink, - credits: editCourseDto.credits, - noOfLessons: editCourseDto.noOfLessons, - language: editCourseDto.language, - duration: editCourseDto.duration, - competency: editCourseDto.competency, - author: editCourseDto.author, + ...editCourseDto, verificationStatus: CourseVerificationStatus.PENDING, - availabilityTime: editCourseDto.availabilityTime } - }) + }); } async getCourse(courseId: number): Promise { @@ -162,7 +151,7 @@ export class CourseService { } - async getUserCourses(courseId: number) { + async getPurchasedUsersByCourseId(courseId: number) { return this.prisma.userCourse.findMany({ where: { @@ -195,7 +184,7 @@ export class CourseService { let course = await this.getCourse(courseId); if(course.verificationStatus != CourseVerificationStatus.PENDING) { - throw new HttpException(`Course is either rejected or is already accepted.`, 406); + throw new NotAcceptableException(`Course is either rejected or is already accepted.`); } return this.prisma.course.update({ where: { id: courseId }, diff --git a/src/course/dto/add-course.dto.ts b/src/course/dto/add-course.dto.ts index 6786db8..4b10442 100644 --- a/src/course/dto/add-course.dto.ts +++ b/src/course/dto/add-course.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { CourseStatus, CourseVerificationStatus } from "@prisma/client"; -import { IsArray, IsDate, IsInt, IsJSON, IsNotEmpty, IsOptional, IsString, Min } from "class-validator"; +import { ArrayNotEmpty, IsArray, IsDate, IsEnum, IsInt, IsJSON, IsNotEmpty, IsNumber, IsOptional, IsString, Min } from "class-validator"; +import { CompetencyMap } from "src/utils/types"; export class AddCourseDto { @@ -43,21 +44,21 @@ export class AddCourseDto { // language @ApiProperty() - @IsNotEmpty() @IsArray() + @ArrayNotEmpty() language: string[]; // course duration @ApiProperty() @IsNotEmpty() @Min(0) - @IsInt() + @IsNumber() duration: number; // competency @ApiProperty() @IsNotEmpty() - competency: any; + competency: CompetencyMap; // author @ApiProperty() @@ -68,7 +69,7 @@ export class AddCourseDto { // course status (active/inactive/archived) @ApiProperty() @IsNotEmpty() - @IsString() + @IsEnum(CourseStatus) status: CourseStatus; // course availability time diff --git a/src/course/dto/completion.dto.ts b/src/course/dto/completion.dto.ts index 86dee32..61f18d2 100644 --- a/src/course/dto/completion.dto.ts +++ b/src/course/dto/completion.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsInt, IsNotEmpty, IsString } from "class-validator"; +import { IsInt, IsNotEmpty, IsUUID } from "class-validator"; export class CompleteCourseDto { @@ -13,6 +13,6 @@ export class CompleteCourseDto { // user ID @ApiProperty() @IsNotEmpty() - @IsString() + @IsUUID() userId: string; } \ No newline at end of file diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index cd3f929..68368d7 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -1,4 +1,6 @@ import { CourseStatus, CourseVerificationStatus } from "@prisma/client"; +import { JsonValue } from "@prisma/client/runtime/library"; +import { CompetencyMap } from "src/utils/types"; export class CourseResponse { @@ -12,7 +14,7 @@ export class CourseResponse { readonly noOfLessons: number | null; readonly language: string[]; readonly duration: number; - readonly competency: any; + readonly competency: JsonValue; readonly author: string; readonly avgRating: number | null; readonly status: CourseStatus; diff --git a/src/course/dto/edit-course.dto.ts b/src/course/dto/edit-course.dto.ts index b88d9db..9224f3b 100644 --- a/src/course/dto/edit-course.dto.ts +++ b/src/course/dto/edit-course.dto.ts @@ -2,6 +2,7 @@ import { ApiProperty, PartialType } from "@nestjs/swagger"; import { AddCourseDto } from "./add-course.dto"; import { IsArray, IsDate, IsInt, IsNotEmpty, IsOptional, IsString, Min } from "class-validator"; import { CourseStatus } from "@prisma/client"; +import { CompetencyMap } from "src/utils/types"; export class EditCourseDto { @@ -58,7 +59,7 @@ export class EditCourseDto { // competency @ApiProperty() @IsOptional() - competency?: any; + competency?: CompetencyMap; // author @ApiProperty() diff --git a/src/course/dto/search.dto.ts b/src/course/dto/search.dto.ts index cbba432..b71ee39 100644 --- a/src/course/dto/search.dto.ts +++ b/src/course/dto/search.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsNotEmpty, IsString } from "class-validator"; +import { CompetencyMap } from "src/utils/types"; export class SearchDto { @@ -11,5 +12,5 @@ export class SearchDto { // competency filters @ApiProperty() - competencies: Record; -} + competencies: CompetencyMap; +} \ No newline at end of file diff --git a/src/provider/dto/login.dto.ts b/src/provider/dto/login.dto.ts index a77c470..e71480b 100644 --- a/src/provider/dto/login.dto.ts +++ b/src/provider/dto/login.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsEmail, IsNotEmpty, IsString } from "class-validator"; +import { IsEmail, IsNotEmpty, IsStrongPassword } from "class-validator"; export class LoginDto { @@ -11,8 +11,8 @@ export class LoginDto { // password @ApiProperty() - @IsNotEmpty() - @IsString() + @IsNotEmpty({ message: 'Password is required' }) + @IsStrongPassword() password: string } diff --git a/src/provider/dto/signup.dto.ts b/src/provider/dto/signup.dto.ts index 4b51e71..ae6e8e7 100644 --- a/src/provider/dto/signup.dto.ts +++ b/src/provider/dto/signup.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsEmail, IsJSON, IsNotEmpty, IsOptional, IsString } from "class-validator"; +import { IsEmail, IsNotEmpty, IsOptional, IsString, IsStrongPassword } from "class-validator"; export class SignupDto { @@ -17,8 +17,18 @@ export class SignupDto { // password @ApiProperty() - @IsNotEmpty() - @IsString() + // A strong password should have a minimum length of 8 characters, + // at least 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special symbol. + @IsNotEmpty({ message: 'Password is required' }) + @IsStrongPassword( + { + minLength: 8, + minUppercase: 1, + minNumbers: 1, + minSymbols: 1, + }, + { message: 'Password is not strong enough' }, + ) password: string // payment info diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index f912fc7..6f7910f 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, HttpException, Injectable, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, Injectable, NotAcceptableException, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { SignupDto } from './dto/signup.dto'; import { PrismaService } from 'src/prisma/prisma.service'; import { ProviderStatus } from '@prisma/client'; @@ -143,7 +143,7 @@ export class ProviderService { if(course.providerId != providerId) throw new BadRequestException("Course does not belong to this provider"); - const userCourses = await this.courseService.getUserCourses(courseId); + const userCourses = await this.courseService.getPurchasedUsersByCourseId(courseId); let feedbacks: Feedback[] = []; for(let u of userCourses) { @@ -167,7 +167,7 @@ export class ProviderService { if(course.providerId != providerId) throw new BadRequestException("Course does not belong to this provider"); - const userCourses = await this.courseService.getUserCourses(courseId); + const userCourses = await this.courseService.getPurchasedUsersByCourseId(courseId); return userCourses.map((u) => { return { @@ -206,7 +206,7 @@ export class ProviderService { let providerInfo = await this.getProvider(providerId); if(providerInfo.status != ProviderStatus.PENDING) { - throw new HttpException(`Provider is either verified or rejected.`, 406); + throw new NotAcceptableException(`Provider is either verified or rejected.`); } return this.prisma.provider.update({ where: {id: providerId}, diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 0000000..6498807 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,2 @@ + +export type CompetencyMap = Record; \ No newline at end of file From 2e32815a7b485600b8e8697ac4dc7711c107297d Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Tue, 14 Nov 2023 19:27:06 +0530 Subject: [PATCH 25/88] Addressed Code review --- src/admin/admin.controller.ts | 5 +++-- src/admin/dto/provider-verify-response.dto.ts | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 src/admin/dto/provider-verify-response.dto.ts diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 4c67578..d02c0ba 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -11,6 +11,7 @@ import { CreditRequest } from './dto/credit-request.dto'; import { json } from 'stream/consumers'; import { ProviderSettlementDto } from './dto/provider-settlement.dto'; import { CourseVerify } from 'src/course/dto/verify-course.dto'; +import { ProviderVerify } from './dto/provider-verify-response.dto'; @Controller('admin') @ApiTags('admin') @@ -162,7 +163,7 @@ export class AdminController { } @ApiOperation({ summary: "Verify provider" }) - @ApiResponse({ status: HttpStatus.OK, type: json }) + @ApiResponse({ status: HttpStatus.OK, type: ProviderVerify }) @Patch('/providers/:providerId/verify') async verifyProvider(@Param("providerId", ParseUUIDPipe) providerId: string, @Res() res) { try { @@ -174,7 +175,7 @@ export class AdminController { res.status(HttpStatus.OK).json({ message: "Verified the provider", - data: response + data: response.id }); } catch (err) { this.logger.error(`Failed to verify the provider with id ${providerId}`); diff --git a/src/admin/dto/provider-verify-response.dto.ts b/src/admin/dto/provider-verify-response.dto.ts new file mode 100644 index 0000000..ae994e1 --- /dev/null +++ b/src/admin/dto/provider-verify-response.dto.ts @@ -0,0 +1,3 @@ +export class ProviderVerify { + readonly providerId: string; +} \ No newline at end of file From 45946484c04f51bb8076726f49f4093afb8a09db Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Tue, 14 Nov 2023 19:51:13 +0530 Subject: [PATCH 26/88] Addressed code review --- package-lock.json | 323 +++++++++++++++++++++++++++++-- package.json | 1 + src/app.module.ts | 4 +- src/auth/auth.module.ts | 8 + src/auth/auth.service.spec.ts | 18 ++ src/auth/auth.service.ts | 14 ++ src/provider/provider.service.ts | 20 +- 7 files changed, 372 insertions(+), 16 deletions(-) create mode 100644 src/auth/auth.module.ts create mode 100644 src/auth/auth.service.spec.ts create mode 100644 src/auth/auth.service.ts diff --git a/package-lock.json b/package-lock.json index e9ea5ea..d5eb665 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@nestjs/swagger": "^7.1.14", "@prisma/client": "^5.4.1", "axios": "^1.6.1", + "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "lodash": "^4.17.21", @@ -1401,6 +1402,47 @@ "node": ">=8" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@nestjs/axios": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.0.tgz", @@ -2646,6 +2688,11 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2697,6 +2744,17 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "8.12.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", @@ -2804,6 +2862,36 @@ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -2965,6 +3053,19 @@ } ] }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3261,6 +3362,14 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -3406,6 +3515,14 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3456,6 +3573,11 @@ "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==" }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -3557,7 +3679,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3610,6 +3731,11 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -3627,6 +3753,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", + "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3733,8 +3867,7 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -4523,6 +4656,33 @@ "node": ">=12" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/fs-monkey": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", @@ -4553,6 +4713,25 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -4728,6 +4907,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -4758,6 +4942,18 @@ "node": ">= 0.8" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -4950,7 +5146,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "engines": { "node": ">=8" } @@ -6081,6 +6276,34 @@ "node": ">=8" } }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -6095,8 +6318,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/multer": { "version": "1.4.4-lts.1", @@ -6263,6 +6485,11 @@ "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==", "dev": true }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -6303,6 +6530,20 @@ "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", "dev": true }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6324,6 +6565,17 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -7170,7 +7422,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -7185,7 +7436,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -7196,8 +7446,7 @@ "node_modules/semver/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/send": { "version": "0.18.0", @@ -7263,6 +7512,11 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -7446,7 +7700,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7616,6 +7869,46 @@ "node": ">=6" } }, + "node_modules/tar": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/terminal-link": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", @@ -8314,6 +8607,14 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/windows-release": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-4.0.0.tgz", diff --git a/package.json b/package.json index 6a0d768..be8db84 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@nestjs/swagger": "^7.1.14", "@prisma/client": "^5.4.1", "axios": "^1.6.1", + "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "lodash": "^4.17.21", diff --git a/src/app.module.ts b/src/app.module.ts index eb6af56..4cadd62 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,6 +7,7 @@ import { ProviderModule } from "./provider/provider.module"; import { AdminModule } from './admin/admin.module'; import { MockWalletModule } from './mock-wallet/mock-wallet.module'; import { CourseModule } from "./course/course.module"; +import { AuthModule } from './auth/auth.module'; @Module({ imports: [ @@ -17,7 +18,8 @@ import { CourseModule } from "./course/course.module"; ProviderModule, AdminModule, MockWalletModule, - CourseModule + CourseModule, + AuthModule ], controllers: [AppController], providers: [AppService], diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts new file mode 100644 index 0000000..eaa7852 --- /dev/null +++ b/src/auth/auth.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { AuthService } from './auth.service'; + +@Module({ + providers: [AuthService], + exports: [AuthService] +}) +export class AuthModule {} diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts new file mode 100644 index 0000000..800ab66 --- /dev/null +++ b/src/auth/auth.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthService], + }).compile(); + + service = module.get(AuthService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts new file mode 100644 index 0000000..cd59e36 --- /dev/null +++ b/src/auth/auth.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; +import * as bcrypt from 'bcrypt'; + +@Injectable() +export class AuthService { + async hashPassword(password: string): Promise { + const saltRounds = 10; + return bcrypt.hash(password, saltRounds); + } + + async comparePasswords(plainTextPassword: string, hashedPassword: string): Promise { + return bcrypt.compare(plainTextPassword, hashedPassword); + } +} \ No newline at end of file diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 6f7910f..8d83aca 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -13,13 +13,16 @@ import { EditCourseDto } from 'src/course/dto/edit-course.dto'; import { EditProvider } from 'src/admin/dto/edit-provider.dto'; import { CourseResponse } from 'src/course/dto/course-response.dto'; import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; +import { AuthService } from 'src/auth/auth.service'; import axios from 'axios'; + @Injectable() export class ProviderService { constructor( private prisma: PrismaService, - private courseService: CourseService + private courseService: CourseService, + private authService: AuthService ) {} async createNewAccount(signupDto: SignupDto) { @@ -28,13 +31,20 @@ export class ProviderService { where : { email: signupDto.email } - }) + }); + if(provider) throw new BadRequestException("Account with that email ID already exists"); + const hashedPassword = await this.authService.hashPassword(signupDto.password); + provider = await this.prisma.provider.create({ data: { - ...signupDto, + name: signupDto.name, + email: signupDto.email, + password: hashedPassword, + paymentInfo: signupDto.paymentInfo ? signupDto.paymentInfo : null + // other user profile data } }); @@ -59,7 +69,9 @@ export class ProviderService { if(!provider) throw new NotFoundException("Email ID does not exist"); - if(provider.password != loginDto.password) + const isPasswordValid = await this.authService.comparePasswords(loginDto.password, provider.password); + + if(!isPasswordValid) throw new BadRequestException("Incorrect password"); return provider.id From 9537afab4e2063fef58b4f4ecb6087922bfd3fda Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Tue, 14 Nov 2023 19:52:13 +0530 Subject: [PATCH 27/88] Debug --- src/provider/provider.module.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/provider/provider.module.ts b/src/provider/provider.module.ts index 7d9ee4d..f45ec31 100644 --- a/src/provider/provider.module.ts +++ b/src/provider/provider.module.ts @@ -4,9 +4,10 @@ import { ProviderService } from './provider.service'; import { CourseService } from 'src/course/course.service'; import { PrismaModule } from 'src/prisma/prisma.module'; import { PrismaService } from 'src/prisma/prisma.service'; +import { AuthModule } from 'src/auth/auth.module'; @Module({ - imports: [PrismaModule], + imports: [PrismaModule, AuthModule], controllers: [ProviderController], providers: [ProviderService, CourseService, PrismaService], exports: [ProviderService] From 177909d24dfa41ac74b662be01c64b4595e7bde8 Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Thu, 16 Nov 2023 13:26:17 +0530 Subject: [PATCH 28/88] Added rejectionReason for provider and course --- prisma/schema.prisma | 2 ++ prisma/seed.ts | 2 ++ src/admin/admin.controller.ts | 35 +++++++++++++++++-- src/admin/admin.service.ts | 8 +++-- src/admin/dto/reject-provider-request.dto.ts | 10 ++++++ src/admin/dto/reject-provider-response.dto.ts | 4 +++ src/course/course.service.ts | 13 +++++-- src/course/dto/course-response.dto.ts | 1 + .../dto/provider-profile-response.dto.ts | 9 +++-- src/provider/provider.service.ts | 14 ++++++++ 10 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 src/admin/dto/reject-provider-request.dto.ts create mode 100644 src/admin/dto/reject-provider-response.dto.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c63eaaa..c54131e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -57,6 +57,7 @@ model Provider { paymentInfo Json? status ProviderStatus @default(PENDING) courses Course[] + rejectionReason String? } @@ -79,6 +80,7 @@ model Course { verificationStatus CourseVerificationStatus @default(PENDING) cqfScore Int? impactScore Float? + rejectionReason String? provider Provider @relation(fields: [providerId], references: [id]) userCourses UserCourse[] } diff --git a/prisma/seed.ts b/prisma/seed.ts index 28d9705..edb6645 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -56,6 +56,7 @@ async function main() { } }, status: 'REJECTED', + rejectionReason: "Invalid backAccNo" // courses: [] } }); @@ -227,6 +228,7 @@ async function main() { status: CourseStatus.ACTIVE, availabilityTime: new Date("2023-10-10"), verificationStatus: CourseVerificationStatus.REJECTED, + rejectionReason: "Level associated with LLVM is wrong" } }); diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index d02c0ba..fbc52e6 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -12,6 +12,8 @@ import { json } from 'stream/consumers'; import { ProviderSettlementDto } from './dto/provider-settlement.dto'; import { CourseVerify } from 'src/course/dto/verify-course.dto'; import { ProviderVerify } from './dto/provider-verify-response.dto'; +import { RejectProviderResponseDto } from './dto/reject-provider-response.dto'; +import { RejectProviderRequestDto } from './dto/reject-provider-request.dto'; @Controller('admin') @ApiTags('admin') @@ -188,6 +190,35 @@ export class AdminController { } } + @ApiOperation({ summary: "Reject provider" }) + @ApiResponse({ status: HttpStatus.OK, type: RejectProviderResponseDto }) + @Patch('/providers/:providerId/reject') + async rejectProvider(@Param("providerId", ParseUUIDPipe) providerId: string, @Body() rejectProviderDto: RejectProviderRequestDto, @Res() res) { + try { + this.logger.log(`Rejecting the provider's account with id ${providerId}`); + + const response = await this.adminService.rejectProvider(providerId, rejectProviderDto.rejectionReason); + + this.logger.log(`Successfully rejected the provider account`); + + res.status(HttpStatus.OK).json({ + message: "Rejected the provider", + data: { + providerId: response.id, + rejectionReason: response.rejectionReason + } + }); + } catch (err) { + this.logger.error(`Failed to reject the provider account with id ${providerId}`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to reject the provider account", + }); + } + } + @ApiOperation({ summary: "Get all the courses"}) @ApiResponse({ status: HttpStatus.OK, type: CourseResponse, isArray: true}) @Get('/courses/') @@ -275,12 +306,12 @@ export class AdminController { @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) @Patch('/courses/:courseId/reject') async rejectCourse ( - @Param("courseId", ParseIntPipe) courseId: number, @Res() res + @Param("courseId", ParseIntPipe) courseId: number, @Body() courseRejectionRequestDto: RejectProviderRequestDto, @Res() res ) { try { this.logger.log(`Processing reject request of course with id ${courseId}`); - const course = await this.adminService.rejectCourse(courseId); + const course = await this.adminService.rejectCourse(courseId, courseRejectionRequestDto.rejectionReason); this.logger.log(`Successfully rejected the course`); diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 01bb9b4..647e749 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -23,6 +23,10 @@ export class AdminService { return this.providerService.verifyProvider(providerId); } + async rejectProvider(providerId: string, rejectionReason: string) { + return this.providerService.rejectProvider(providerId, rejectionReason); + } + async findAllProviders(): Promise[]> { return this.providerService.fetchAllProviders(); @@ -48,9 +52,9 @@ export class AdminService { return this.courseService.acceptCourse(courseId, cqf_score); } - async rejectCourse(courseId: number) { + async rejectCourse(courseId: number, rejectionReason: string) { - return this.courseService.rejectCourse(courseId); + return this.courseService.rejectCourse(courseId, rejectionReason); } async removeCourse(courseId: number) { diff --git a/src/admin/dto/reject-provider-request.dto.ts b/src/admin/dto/reject-provider-request.dto.ts new file mode 100644 index 0000000..aba3668 --- /dev/null +++ b/src/admin/dto/reject-provider-request.dto.ts @@ -0,0 +1,10 @@ +import { IsNotEmpty, IsString } from "class-validator"; +import { ApiProperty } from "@nestjs/swagger"; + + +export class RejectProviderRequestDto { + @ApiProperty() + @IsNotEmpty() + @IsString() + rejectionReason: string +} \ No newline at end of file diff --git a/src/admin/dto/reject-provider-response.dto.ts b/src/admin/dto/reject-provider-response.dto.ts new file mode 100644 index 0000000..8cfe17f --- /dev/null +++ b/src/admin/dto/reject-provider-response.dto.ts @@ -0,0 +1,4 @@ +export class RejectProviderResponseDto { + readonly providerId: string; + readonly rejectionReason: string; +} \ No newline at end of file diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 7f536da..8703776 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -195,13 +195,20 @@ export class CourseService { }); } - async rejectCourse(courseId: number) { + async rejectCourse(courseId: number, rejectionReason: string) { - await this.getCourse(courseId); + const course = await this.getCourse(courseId); + + if(course.verificationStatus != CourseVerificationStatus.PENDING) { + throw new NotAcceptableException(`Course is already rejected or is accepted`); + } return this.prisma.course.update({ where: {id: courseId}, - data: {verificationStatus: CourseVerificationStatus.REJECTED} + data: { + verificationStatus: CourseVerificationStatus.REJECTED, + rejectionReason: rejectionReason + } }); } diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index 68368d7..9ef2601 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -20,6 +20,7 @@ export class CourseResponse { readonly status: CourseStatus; readonly availabilityTime: Date | null; readonly verificationStatus: CourseVerificationStatus; + readonly rejectionReason: string | null; } export class AdminCourseResponse extends CourseResponse { diff --git a/src/provider/dto/provider-profile-response.dto.ts b/src/provider/dto/provider-profile-response.dto.ts index d41c0df..7c348b5 100644 --- a/src/provider/dto/provider-profile-response.dto.ts +++ b/src/provider/dto/provider-profile-response.dto.ts @@ -27,11 +27,16 @@ export class ProviderProfileResponse { @ApiProperty() @IsOptional() - paymentInfo: any; + paymentInfo?: any; @ApiProperty() @IsOptional() @IsEnum(ProviderStatus) - status: ProviderStatus; + status?: ProviderStatus; + + @ApiProperty() + @IsOptional() + @IsString() + rejectionReason?: string; // readonly courses: Course[]; } \ No newline at end of file diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 8d83aca..1abfad3 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -225,4 +225,18 @@ export class ProviderService { data: {status: ProviderStatus.VERIFIED} }); } + + async rejectProvider(providerId: string, rejectionReason: string) { + let providerInfo = await this.getProvider(providerId); + if(providerInfo.status != ProviderStatus.PENDING) { + throw new NotAcceptableException(`Provider is either already accepted or rejected`); + } + return this.prisma.provider.update({ + where: {id: providerId}, + data: { + status: ProviderStatus.REJECTED, + rejectionReason: rejectionReason + } + }); + } } \ No newline at end of file From 3ac610107573ce756a3df878f5077a3391dde811 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Mon, 20 Nov 2023 16:14:09 +0530 Subject: [PATCH 29/88] minor changes and fixes --- src/admin/dto/credit-request.dto.ts | 4 +-- src/admin/dto/provider-settlement.dto.ts | 4 +-- src/course/course.controller.ts | 6 ++-- src/course/course.service.ts | 36 +++++++++++++------ .../dto/provider-profile-response.dto.ts | 4 +-- src/utils/utils.ts | 34 ++++++++++++------ 6 files changed, 57 insertions(+), 31 deletions(-) diff --git a/src/admin/dto/credit-request.dto.ts b/src/admin/dto/credit-request.dto.ts index 32acf4f..de19234 100644 --- a/src/admin/dto/credit-request.dto.ts +++ b/src/admin/dto/credit-request.dto.ts @@ -1,11 +1,11 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsEmail, IsInt, IsJSON, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsInt, IsNotEmpty, IsNumber, IsUUID } from 'class-validator'; export class CreditRequest { @ApiProperty() @IsNotEmpty() - @IsNumber() + @IsUUID() readonly providerId: string @ApiProperty() diff --git a/src/admin/dto/provider-settlement.dto.ts b/src/admin/dto/provider-settlement.dto.ts index 895e6c1..9c69e39 100644 --- a/src/admin/dto/provider-settlement.dto.ts +++ b/src/admin/dto/provider-settlement.dto.ts @@ -1,10 +1,10 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; export class ProviderSettlementDto { @ApiProperty({required: false}) - @IsNumber() + @IsUUID() id: string; @ApiProperty() diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index c3b21a6..648138d 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, HttpStatus, Logger, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Res } from "@nestjs/common"; +import { Body, Controller, Get, HttpStatus, Logger, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Query, Res } from "@nestjs/common"; import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { CourseService } from "./course.service"; import { FeedbackDto } from "./dto/feedback.dto"; @@ -18,13 +18,13 @@ export class CourseController { @Get("/search") // search courses async searchCourses( - // @Body() searchDto: SearchDto, + @Query('searchInput') searchInput: string, @Res() res ) { try { this.logger.log(`Getting information of courses`); - const courses = await this.courseService.searchCourses(); + const courses = await this.courseService.searchCourses(searchInput); this.logger.log(`Successfully retrieved the courses`); diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 8703776..a5636c0 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -13,18 +13,32 @@ export class CourseService { private prisma: PrismaService, ) {} - async searchCourses(): Promise { - + async searchCourses(searchInput: string): Promise { const courses = await this.prisma.course.findMany({ - // where: { - // title: { - // contains: searchDto.searchInput, - // mode: "insensitive", - // } - // } - }) - - + where: { + OR: [{ + title: { + contains: searchInput, + mode: "insensitive", + } + }, { + author: { + contains: searchInput, + mode: "insensitive", + } + }, { + description: { + contains: searchInput, + mode: "insensitive", + } + }, { + competency: { + string_contains: searchInput + } + }] + + } + }); return courses; } diff --git a/src/provider/dto/provider-profile-response.dto.ts b/src/provider/dto/provider-profile-response.dto.ts index 7c348b5..32c9999 100644 --- a/src/provider/dto/provider-profile-response.dto.ts +++ b/src/provider/dto/provider-profile-response.dto.ts @@ -1,11 +1,11 @@ import { ApiProperty } from "@nestjs/swagger"; import { ProviderStatus } from "@prisma/client"; -import { IsEmail, IsEnum, IsNumber, IsOptional, IsString } from 'class-validator'; +import { IsEmail, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator'; export class ProviderProfileResponse { @ApiProperty({required: false}) - @IsNumber() + @IsUUID() @IsOptional() id: string; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index accd650..f108b93 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,5 +1,9 @@ import { HttpStatus } from "@nestjs/common"; -import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; +import { + PrismaClientKnownRequestError, + PrismaClientValidationError, +} from "@prisma/client/runtime/library"; +import { get } from "lodash"; export const validationOptions = { whitelist: true, @@ -14,25 +18,33 @@ export function getPrismaErrorStatusAndMessage(error: any): { errorMessage: string | undefined; statusCode: number; } { - if(error instanceof PrismaClientKnownRequestError) { - const errorCode = error?.code || "DEFAULT_ERROR_CODE"; + if ( + error instanceof PrismaClientKnownRequestError || + error instanceof PrismaClientValidationError + ) { + const errorCode = get(error, "code", "DEFAULT_ERROR_CODE"); + const errorCodeMap: Record = { P2000: HttpStatus.BAD_REQUEST, P2002: HttpStatus.CONFLICT, P2003: HttpStatus.CONFLICT, P2025: HttpStatus.NOT_FOUND, + DEFAULT_ERROR_CODE: HttpStatus.INTERNAL_SERVER_ERROR, }; - const statusCode = errorCodeMap[errorCode] || HttpStatus.INTERNAL_SERVER_ERROR; - const errorMessage = error.message.split('\n').pop(); + const statusCode = errorCodeMap[errorCode]; + const errorMessage = error.message.split("\n").pop(); + return { statusCode, errorMessage }; } - const statusCode = - error?.status || error?.response?.status || HttpStatus.INTERNAL_SERVER_ERROR; - + const statusCode = + error?.response?.data?.statusCode || + error?.status || + error?.response?.status || + HttpStatus.INTERNAL_SERVER_ERROR; return { - statusCode, - errorMessage: error.message, + statusCode, + errorMessage: error?.response?.data?.message || error?.message, }; -} +} \ No newline at end of file From 0e3019c2414c355260633a38b76d70bec9565b75 Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Mon, 20 Nov 2023 16:26:14 +0530 Subject: [PATCH 30/88] Added comments --- src/admin/admin.service.ts | 17 +++++++++++++++++ src/auth/auth.service.ts | 2 ++ 2 files changed, 19 insertions(+) diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 647e749..3c076f8 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -18,50 +18,60 @@ export class AdminService { private courseService: CourseService ) {} + // verify provider account async verifyProvider(providerId: string) { return this.providerService.verifyProvider(providerId); } + // reject provider account async rejectProvider(providerId: string, rejectionReason: string) { return this.providerService.rejectProvider(providerId, rejectionReason); } + // fetch all providers on marketplace async findAllProviders(): Promise[]> { return this.providerService.fetchAllProviders(); } + // fetch provider with the given id async findProviderById(providerId: string): Promise { return this.providerService.getProvider(providerId); } + // fetch all courses added onto the marketplace async findAllCourses() : Promise { return this.courseService.fetchAllCourses(); } + // find course by Id async findCourseById(courseId: number) { return this.courseService.getCourse(courseId); } + // accept a course along with the cqf score async acceptCourse(courseId: number, cqf_score: number) { return this.courseService.acceptCourse(courseId, cqf_score); } + // reject a course async rejectCourse(courseId: number, rejectionReason: string) { return this.courseService.rejectCourse(courseId, rejectionReason); } + // remove a course from marketplace async removeCourse(courseId: number) { return this.courseService.removeCourse(courseId); } + // get all admin-consumer transactions async getTransactions(adminId: string) { const walletService = process.env.WALLET_SERVICE_URL; @@ -76,6 +86,7 @@ export class AdminService { } } + // Add or remove credits to provider wallet async addOrRemoveCreditsToProvider(adminId: string, providerId: string, credits: number) { const walletService = process.env.WALLET_SERVICE_URL; let endpoint: string; @@ -97,11 +108,13 @@ export class AdminService { } } + // edit provider profile information async editProviderProfile(profileInfo: EditProvider) { return this.providerService.editProviderProfileByAdmin(profileInfo); } + // Get number of course purchases for a provider async getNoOfCoursePurchasesForProvider(providerId: string) { return await this.prisma.userCourse.count({ where: { @@ -112,6 +125,7 @@ export class AdminService { }); } + // Get the number of courses added by a provider async getNumberOfCoursesForProvider(providerId: string) { return await this.prisma.course.count({ where: { @@ -120,6 +134,7 @@ export class AdminService { }); } + // Get the number of credits in a provider wallet async getProviderWalletCredits(providerId: string) { const url = process.env.WALLET_SERVICE_URL; const endpoint = url + `/api/providers/${providerId}/credits`; @@ -127,6 +142,7 @@ export class AdminService { return resp.data.credits; } + // Get all the providers information for settlement async getAllProviderInfoForSettlement() { const providers = await this.providerService.fetchAllProviders(); @@ -143,6 +159,7 @@ export class AdminService { return results; } + // settle credits for a provider async settleCredits(adminId: string, providerId: string) { // Need to add transaction, add paymentReceipt additional settlement processing // then set the credits of the provider to 0 diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index cd59e36..a2f3fc2 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -3,6 +3,8 @@ import * as bcrypt from 'bcrypt'; @Injectable() export class AuthService { + + // generate hash for a given password async hashPassword(password: string): Promise { const saltRounds = 10; return bcrypt.hash(password, saltRounds); From f8edf5758ae763fcbfea1e97b3452b42eb506875 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Wed, 22 Nov 2023 11:41:33 +0530 Subject: [PATCH 31/88] course transactions --- package-lock.json | 10 +++ package.json | 1 + prisma/schema.prisma | 8 +-- prisma/seed.ts | 23 +++---- src/course/course.controller.ts | 2 +- src/course/course.service.ts | 90 ++++++++++++++++++++------- src/course/dto/add-course.dto.ts | 2 +- src/course/dto/course-response.dto.ts | 3 +- src/course/dto/edit-course.dto.ts | 2 +- src/course/dto/transaction.dto.ts | 26 ++++++++ src/provider/dto/purchase.dto.ts | 14 ----- src/provider/provider.controller.ts | 23 ++++--- src/provider/provider.service.ts | 78 ++++++++++++++--------- 13 files changed, 182 insertions(+), 100 deletions(-) create mode 100644 src/course/dto/transaction.dto.ts delete mode 100644 src/provider/dto/purchase.dto.ts diff --git a/package-lock.json b/package-lock.json index d5eb665..b0962d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@nestjs/cli": "^9.0.0", "@nestjs/schematics": "^9.0.0", "@nestjs/testing": "^9.0.0", + "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.13", "@types/jest": "28.1.8", "@types/lodash": "^4.14.200", @@ -2098,6 +2099,15 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.3", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.3.tgz", diff --git a/package.json b/package.json index be8db84..53a6ee0 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "@nestjs/cli": "^9.0.0", "@nestjs/schematics": "^9.0.0", "@nestjs/testing": "^9.0.0", + "@types/bcrypt": "^5.0.2", "@types/express": "^4.17.13", "@types/jest": "28.1.8", "@types/lodash": "^4.14.200", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c54131e..de4824a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -31,8 +31,7 @@ enum CourseVerificationStatus { } enum CourseStatus { - ACTIVE - INACTIVE + UNARCHIVED ARCHIVED } @@ -75,8 +74,9 @@ model Course { competency Json author String avgRating Float? - status CourseStatus - availabilityTime DateTime? + status CourseStatus @default(UNARCHIVED) + startDate DateTime? + endDate DateTime? verificationStatus CourseVerificationStatus @default(PENDING) cqfScore Int? impactScore Float? diff --git a/prisma/seed.ts b/prisma/seed.ts index edb6645..ffff911 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -78,8 +78,8 @@ async function main() { "Backend engineering": ["Level1"] }, author: "Stephen Grider", - status: "ACTIVE", - availabilityTime: new Date("2024-05-01").toISOString() + startDate: new Date("2024-05-01").toISOString(), + endDate: new Date("2024-07-01").toISOString() }, { providerId: provider1.id, title: "Graphic Design Masterclass", @@ -95,8 +95,8 @@ async function main() { "Understanding brand": ["Level1"] }, author: "Lindsay Marsh", - status: "ACTIVE", - availabilityTime: new Date("2024-05-01").toISOString() + startDate: new Date("2024-05-01").toISOString(), + endDate: new Date("2024-09-01").toISOString() }, { providerId: provider1.id, title: "Python for Data Science", @@ -113,8 +113,6 @@ async function main() { "MySQL": ["Level1"] }, author: "Jose Portilla", - status: "ACTIVE", - availabilityTime: new Date("2024-05-01").toISOString() }, { providerId: response.id, title: "Microsoft Excel", @@ -129,8 +127,7 @@ async function main() { "Excel": ["Level1", "Level2", "Level3", "Level4"] }, author: "Kyle Pew", - status: "ACTIVE", - availabilityTime: new Date("2024-05-01").toISOString() + startDate: new Date("2024-05-01").toISOString() }] }) @@ -180,8 +177,8 @@ async function main() { "Orchestration": [ "Level5" ] }, author: "Jason Frig", - status: CourseStatus.ACTIVE, - availabilityTime: new Date("2023-06-01"), + startDate: new Date("2023-06-01"), + endDate: new Date("2023-08-01"), verificationStatus: CourseVerificationStatus.ACCEPTED, cqfScore: 10, } @@ -203,8 +200,6 @@ async function main() { "Python": [ "Level1", "Level2" ] }, author: "James Franco", - status: CourseStatus.ACTIVE, - availabilityTime: new Date("2023-08-10"), verificationStatus: CourseVerificationStatus.PENDING, } }); @@ -225,8 +220,8 @@ async function main() { "LLVM": [ "Level4" ] }, author: "Ramakrishna Upadrasta", - status: CourseStatus.ACTIVE, - availabilityTime: new Date("2023-10-10"), + startDate: new Date("2023-10-10"), + endDate: new Date("2023-11-10"), verificationStatus: CourseVerificationStatus.REJECTED, rejectionReason: "Level associated with LLVM is wrong" } diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index 648138d..9cc2526 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -85,7 +85,7 @@ export class CourseController { try { this.logger.log(`Recording the user purchase of the course`); - await this.courseService.insertUserCourse(courseId, userId); + await this.courseService.addPurchaseRecord(courseId, userId); this.logger.log(`Successfully recorded the purchase`); diff --git a/src/course/course.service.ts b/src/course/course.service.ts index a5636c0..05288fd 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -6,6 +6,7 @@ import { Course, CourseProgressStatus, CourseStatus, CourseVerificationStatus } import { CompleteCourseDto } from "./dto/completion.dto"; import { EditCourseDto } from "./dto/edit-course.dto"; import { AdminCourseResponse, CourseResponse } from "src/course/dto/course-response.dto"; +import { CourseTransactionDto } from "./dto/transaction.dto"; @Injectable() export class CourseService { @@ -14,6 +15,9 @@ export class CourseService { ) {} async searchCourses(searchInput: string): Promise { + + // Searches for the courses available in the DB that match or contain the input search string + // in their title, author, description or competency const courses = await this.prisma.course.findMany({ where: { OR: [{ @@ -36,7 +40,6 @@ export class CourseService { string_contains: searchInput } }] - } }); return courses; @@ -44,6 +47,7 @@ export class CourseService { async addCourse(providerId: string, addCourseDto: AddCourseDto) { + // add new course to the platform return await this.prisma.course.create({ data: { providerId, @@ -52,16 +56,16 @@ export class CourseService { }); } - async addPurchaseRecord(userId: string, courseId: number) { + async addPurchaseRecord(courseId: number, userId: string) { + // Check if course already purchased const record = this.prisma.userCourse.findFirst({ where: { userId: userId, courseId: courseId } }); - - if(record != null) { + if(record != null) throw new BadRequestException("Course already purchased by the user"); - } + // create new record for purchase return await this.prisma.userCourse.create({ data: { userId, @@ -70,8 +74,9 @@ export class CourseService { }); } - async archiveCourse(providerId: string, courseId: number) { + async archiveCourse(courseId: number) { + // update the course status to archived return this.prisma.course.update({ where: { id: courseId }, data: { status: CourseStatus.ARCHIVED } @@ -79,9 +84,10 @@ export class CourseService { } - async editCourse(providerId: string, courseId: number, editCourseDto: EditCourseDto) { + async editCourse(courseId: number, editCourseDto: EditCourseDto) { - return await this.prisma.course.update({ + // update the course details as required and change its verification status to pending + return this.prisma.course.update({ where: { id: courseId }, data: { ...editCourseDto, @@ -92,6 +98,7 @@ export class CourseService { async getCourse(courseId: number): Promise { + // Find course by ID and throw error if not found const course = await this.prisma.course.findUnique({ where: { id: courseId @@ -103,22 +110,13 @@ export class CourseService { return course; } - async insertUserCourse(courseId: number, userId: string) { - - await this.getCourse(courseId); - - await this.prisma.userCourse.create({ - data: { - courseId, - userId - } - }) - } - async giveCourseFeedback(courseId: number, userId: string, feedbackDto: FeedbackDto) { + // Validate course await this.getCourse(courseId); + // Find purchase record with consumer Id and course ID and throw error if not found + // Or if course not complete const userCourse = await this.prisma.userCourse.findUnique({ where: { userId_courseId: { @@ -133,6 +131,7 @@ export class CourseService { if(userCourse.status != CourseProgressStatus.COMPLETED) throw new BadRequestException("Course not complete"); + // Add feedback await this.prisma.userCourse.update({ where: { userId_courseId: { @@ -148,6 +147,7 @@ export class CourseService { async deleteCourse(courseId: number) { + // Delete the course entry from db await this.prisma.course.delete({ where: { id: courseId @@ -157,6 +157,7 @@ export class CourseService { async getProviderCourses(providerId: string) { + // Get all courses added by a single provider return this.prisma.course.findMany({ where: { providerId @@ -167,6 +168,7 @@ export class CourseService { async getPurchasedUsersByCourseId(courseId: number) { + // Get all users that have bought a course return this.prisma.userCourse.findMany({ where: { courseId @@ -176,6 +178,7 @@ export class CourseService { async markCourseComplete(completeCourseDto: CompleteCourseDto) { + // Update a course as complete for a purchased course await this.prisma.userCourse.update({ where: { userId_courseId: { @@ -190,16 +193,20 @@ export class CourseService { async fetchAllCourses() : Promise { + // Fetch all courses return this.prisma.course.findMany(); } async acceptCourse(courseId: number, cqf_score: number) { + // Validate course let course = await this.getCourse(courseId); + // Check if the course verfication is pending if(course.verificationStatus != CourseVerificationStatus.PENDING) { throw new NotAcceptableException(`Course is either rejected or is already accepted.`); } + // Update the course as accepted return this.prisma.course.update({ where: { id: courseId }, data: { @@ -211,12 +218,14 @@ export class CourseService { async rejectCourse(courseId: number, rejectionReason: string) { + // Validate course const course = await this.getCourse(courseId); + // Check if the course verfication is pending if(course.verificationStatus != CourseVerificationStatus.PENDING) { throw new NotAcceptableException(`Course is already rejected or is accepted`); } - + // Reject the course return this.prisma.course.update({ where: {id: courseId}, data: { @@ -227,11 +236,48 @@ export class CourseService { } async removeCourse(courseId: number) { - + + // Validate course await this.getCourse(courseId); + // Delete course entry return this.prisma.course.delete({ where: {id: courseId} }); } + + async getCourseTransactions(providerId: string): Promise { + + // Fetch course details and number of purchases + const transactions = await this.prisma.course.findMany({ + where: { + providerId + }, + select: { + id: true, + title: true, + startDate: true, + endDate: true, + credits: true, + _count: { + select: { + userCourses: true + } + } + }, + }); + + // Refactor to the DTO format required + return transactions.map((c) => { + return { + courseId: c.id, + courseName: c.title, + startDate: c.startDate, + endDate: c.endDate, + credits: c.credits, + numConsumersEnrolled: c._count.userCourses, + income: c.credits * c._count.userCourses + } + }); + } } diff --git a/src/course/dto/add-course.dto.ts b/src/course/dto/add-course.dto.ts index 4b10442..611d97c 100644 --- a/src/course/dto/add-course.dto.ts +++ b/src/course/dto/add-course.dto.ts @@ -66,7 +66,7 @@ export class AddCourseDto { @IsString() author: string; - // course status (active/inactive/archived) + // course status (archived/unarchived) @ApiProperty() @IsNotEmpty() @IsEnum(CourseStatus) diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index 9ef2601..e142788 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -18,7 +18,8 @@ export class CourseResponse { readonly author: string; readonly avgRating: number | null; readonly status: CourseStatus; - readonly availabilityTime: Date | null; + readonly startDate: Date | null; + readonly endDate: Date | null; readonly verificationStatus: CourseVerificationStatus; readonly rejectionReason: string | null; } diff --git a/src/course/dto/edit-course.dto.ts b/src/course/dto/edit-course.dto.ts index 9224f3b..2f25d7f 100644 --- a/src/course/dto/edit-course.dto.ts +++ b/src/course/dto/edit-course.dto.ts @@ -67,7 +67,7 @@ export class EditCourseDto { @IsOptional() author?: string; - // course status (active/inactive/archived) + // course status (archived/unarchived) // @ApiProperty() // @IsString() // @IsOptional() diff --git a/src/course/dto/transaction.dto.ts b/src/course/dto/transaction.dto.ts new file mode 100644 index 0000000..e4224aa --- /dev/null +++ b/src/course/dto/transaction.dto.ts @@ -0,0 +1,26 @@ + + +export class CourseTransactionDto { + + // course ID + readonly courseId: number; + + // course name + readonly courseName: string; + + // timestamp for when the course availability starts + readonly startDate: Date | null; + + // timestamp for when the course availability ends + readonly endDate: Date | null; + + // Credit cost of course + readonly credits: number; + + // Number of enrolled consumers + readonly numConsumersEnrolled: number; + + // Total income from course + readonly income: number; +} + diff --git a/src/provider/dto/purchase.dto.ts b/src/provider/dto/purchase.dto.ts deleted file mode 100644 index 52e6288..0000000 --- a/src/provider/dto/purchase.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ - - -export class PurchaseResponseDto { - - // user ID - readonly userId: string; - - // course ID - readonly courseId: number; - - // timestamp for when the course was purchased - readonly purchasedAt: Date; -} - diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index 06a2028..63798a4 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -6,7 +6,7 @@ import { LoginDto, LoginResponseDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { AddCourseDto, AddCourseResponseDto } from 'src/course/dto/add-course.dto'; import { FeedbackResponseDto } from './dto/feedback.dto'; -import { PurchaseResponseDto } from './dto/purchase.dto'; +import { CourseTransactionDto } from '../course/dto/transaction.dto'; import { CompleteCourseDto } from 'src/course/dto/completion.dto'; import { EditCourseDto } from 'src/course/dto/edit-course.dto'; import { CourseResponse } from 'src/course/dto/course-response.dto'; @@ -331,25 +331,24 @@ export class ProviderController { } } - @ApiOperation({ summary: 'Get all transactions for course purchase user wise' }) - @ApiResponse({ status: HttpStatus.OK, type: [PurchaseResponseDto] }) - @Get("/:providerId/course/:courseId/purchases") - // Get all the transactions for course purchase user wise - async getCoursePurchases( + @ApiOperation({ summary: 'Get transactions of all courses' }) + @ApiResponse({ status: HttpStatus.OK, type: [CourseTransactionDto] }) + @Get("/:providerId/course/transactions") + // Get transactions of all courses + async getCourseTransactions( @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, @Res() res ) { try { - this.logger.log(`Getting course purchase transactions`); + this.logger.log(`Getting course transactions`); - const purchaseResponse = await this.providerService.getCoursePurchases(providerId, courseId); + const transactionsResponse = await this.providerService.getCourseTransactions(providerId); - this.logger.log(`Successfully retrieved course purchase transactions`); + this.logger.log(`Successfully retrieved course transactions`); res.status(HttpStatus.OK).json({ - message: "purchases fetched successfully", - data: purchaseResponse + message: "transactions fetched successfully", + data: transactionsResponse }) } catch (err) { this.logger.error(`Failed to fetch the transactions`); diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 1abfad3..b175e3d 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -7,7 +7,7 @@ import { UpdateProfileDto } from './dto/update-profile.dto'; import { AddCourseDto } from 'src/course/dto/add-course.dto'; import { CourseService } from 'src/course/course.service'; import { Feedback, FeedbackResponseDto } from './dto/feedback.dto'; -import { PurchaseResponseDto } from './dto/purchase.dto'; +import { CourseTransactionDto } from '../course/dto/transaction.dto'; import { CompleteCourseDto } from 'src/course/dto/completion.dto'; import { EditCourseDto } from 'src/course/dto/edit-course.dto'; import { EditProvider } from 'src/admin/dto/edit-provider.dto'; @@ -27,6 +27,7 @@ export class ProviderService { async createNewAccount(signupDto: SignupDto) { + // Check if email already exists let provider = await this.prisma.provider.findUnique({ where : { email: signupDto.email @@ -36,8 +37,10 @@ export class ProviderService { if(provider) throw new BadRequestException("Account with that email ID already exists"); + // Hashing the password const hashedPassword = await this.authService.hashPassword(signupDto.password); + // Create an entry in the database provider = await this.prisma.provider.create({ data: { name: signupDto.name, @@ -48,6 +51,7 @@ export class ProviderService { } }); + // Forward to wallet service for creation of wallet const url = process.env.WALLET_SERVICE_URL; const endpoint = url + `/api/wallet/create`; const reqBody = { @@ -61,6 +65,7 @@ export class ProviderService { async getProviderIdFromLogin(loginDto: LoginDto) { + // Fetch the provider from email ID const provider = await this.prisma.provider.findUnique({ where: { email: loginDto.email @@ -69,6 +74,7 @@ export class ProviderService { if(!provider) throw new NotFoundException("Email ID does not exist"); + // Compare the entered password with the password fetched from database const isPasswordValid = await this.authService.comparePasswords(loginDto.password, provider.password); if(!isPasswordValid) @@ -79,6 +85,7 @@ export class ProviderService { async getProvider(providerId: string) { + // Fetch provider details using ID const provider = await this.prisma.provider.findUnique({ where: { id: providerId @@ -90,20 +97,18 @@ export class ProviderService { return provider; } + // Used when provider makes a request to update profile async updateProfileInfo(providerId: string, updateProfileDto: UpdateProfileDto) { - try { - await this.prisma.provider.update({ - where: { - id: providerId - }, - data: updateProfileDto - }) - } catch { - throw new NotFoundException("profile does not exist"); - } + await this.prisma.provider.update({ + where: { + id: providerId + }, + data: updateProfileDto + }) } + // Used when admin makes a request to update provider profile async editProviderProfileByAdmin(profileInfo: EditProvider) { return this.prisma.provider.update({ @@ -114,16 +119,20 @@ export class ProviderService { async addNewCourse(providerId: string, addCourseDto: AddCourseDto) { + // Fetch provider const provider = await this.getProvider(providerId); + // Check verification if(provider.status != ProviderStatus.VERIFIED) throw new UnauthorizedException("Provider account is not verified"); + // Forward to course service return this.courseService.addCourse(providerId, addCourseDto); } async removeCourse(providerId: string, courseId: number) { + // Validate course ID provided const course = await this.courseService.getCourse(courseId); if(!course) throw new NotFoundException("Course does not exist"); @@ -131,6 +140,7 @@ export class ProviderService { if(course.providerId != providerId) throw new BadRequestException("Course does not belong to this provider"); + // Forward to course service await this.courseService.deleteCourse(courseId); } @@ -140,23 +150,34 @@ export class ProviderService { } async editCourse(providerId: string, courseId: number, editCourseDto: EditCourseDto) { - const course = await this.courseService.editCourse(providerId, courseId, editCourseDto); - return course; + + // Validate provider + await this.getProvider(providerId); + + return this.courseService.editCourse(courseId, editCourseDto); } async archiveCourse(providerId: string, courseId: number) { - return this.courseService.archiveCourse(providerId, courseId); + + // Validate provider + await this.getProvider(providerId); + + return this.courseService.archiveCourse(courseId); } async getCourseFeedbacks(providerId: string, courseId: number): Promise { + // Fetch course const course = await this.courseService.getCourse(courseId); + // Validate course with provider if(course.providerId != providerId) throw new BadRequestException("Course does not belong to this provider"); + // Forward to course service const userCourses = await this.courseService.getPurchasedUsersByCourseId(courseId); + // Construction of DTO required for response let feedbacks: Feedback[] = []; for(let u of userCourses) { if(u.feedback && u.rating) { @@ -172,27 +193,14 @@ export class ProviderService { }; } - async getCoursePurchases(providerId: string, courseId: number): Promise { - - const course = await this.courseService.getCourse(courseId); - - if(course.providerId != providerId) - throw new BadRequestException("Course does not belong to this provider"); - - const userCourses = await this.courseService.getPurchasedUsersByCourseId(courseId); - - return userCourses.map((u) => { - return { - courseId: u.courseId, - purchasedAt: u.purchasedAt, - userId: u.userId - } - }) + async getCourseTransactions(providerId: string): Promise { + return this.courseService.getCourseTransactions(providerId) } async markCourseComplete(providerId: string, completeCourseDto: CompleteCourseDto) { + // Validate course ID provided const course = await this.courseService.getCourse(completeCourseDto.courseId); if(!course) throw new NotFoundException("Course does not exist"); @@ -200,6 +208,7 @@ export class ProviderService { if(course.providerId != providerId) throw new BadRequestException("Course does not belong to this provider"); + // Forward to course service. Error is thrown when user has not purchased a course try { await this.courseService.markCourseComplete(completeCourseDto); } catch { @@ -215,11 +224,15 @@ export class ProviderService { } async verifyProvider(providerId: string) { + + // Fetch provider let providerInfo = await this.getProvider(providerId); + // Check if provider verification is pending if(providerInfo.status != ProviderStatus.PENDING) { throw new NotAcceptableException(`Provider is either verified or rejected.`); } + // Update the status in database return this.prisma.provider.update({ where: {id: providerId}, data: {status: ProviderStatus.VERIFIED} @@ -227,10 +240,15 @@ export class ProviderService { } async rejectProvider(providerId: string, rejectionReason: string) { + + // Fetch provider let providerInfo = await this.getProvider(providerId); + + // Check if provider verification is pending if(providerInfo.status != ProviderStatus.PENDING) { throw new NotAcceptableException(`Provider is either already accepted or rejected`); } + // Update the status in database return this.prisma.provider.update({ where: {id: providerId}, data: { From 7aec438edd2f8096a246d040fabdcf56a1d417c3 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 24 Nov 2023 10:53:24 +0530 Subject: [PATCH 32/88] api changes --- prisma/schema.prisma | 5 +- prisma/seed.ts | 19 +++-- src/admin/dto/provider-settlement.dto.ts | 4 +- src/course/course.service.ts | 20 ++++- src/course/dto/add-course.dto.ts | 6 +- src/course/dto/edit-course.dto.ts | 6 +- src/course/dto/feedback.dto.ts | 6 +- src/provider/dto/login.dto.ts | 11 ++- .../dto/provider-profile-response.dto.ts | 37 ++++++--- src/provider/dto/signup.dto.ts | 26 ++++++- src/provider/dto/update-profile.dto.ts | 24 +++++- src/provider/provider.controller.ts | 30 +++++++- src/provider/provider.service.ts | 76 +++++++++++++------ src/utils/types.ts | 11 ++- 14 files changed, 222 insertions(+), 59 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index de4824a..5dea5ea 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -52,7 +52,10 @@ model Provider { id String @id @default(uuid()) name String email String @unique - password String + password String + orgName String + orgLogo String + phone String paymentInfo Json? status ProviderStatus @default(PENDING) courses Course[] diff --git a/prisma/seed.ts b/prisma/seed.ts index ffff911..03752aa 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -8,7 +8,10 @@ async function main() { name: "Vijay Salgaonkar", email: "vijaysalgaonkar@gmail.com", password: "9d209aacaed4088d68c41bd8dfb20de39cbd8339", - status: ProviderStatus.VERIFIED + status: ProviderStatus.VERIFIED, + orgLogo: "https://logos-world.net/wp-content/uploads/2021/11/Udemy-Logo.png", + orgName: "Udemy", + phone: "9999999999", } }); @@ -24,7 +27,9 @@ async function main() { } }, status: 'VERIFIED', - // courses: [] + orgLogo: "https://logos-world.net/wp-content/uploads/2021/11/Udemy-Logo.png", + orgName: "Udemy", + phone: "9999999999", } }); @@ -40,7 +45,9 @@ async function main() { } }, status: 'PENDING', - // courses: [] + orgLogo: "https://1000logos.net/wp-content/uploads/2022/06/Coursera-Logo-2012.png", + orgName: "Coursera", + phone: "9999999999", } }); @@ -56,8 +63,10 @@ async function main() { } }, status: 'REJECTED', - rejectionReason: "Invalid backAccNo" - // courses: [] + rejectionReason: "Invalid backAccNo", + orgLogo: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Logo_of_Twitter.svg/2491px-Logo_of_Twitter.svg.png", + orgName: "Sunbird", + phone: "9999999999", } }); diff --git a/src/admin/dto/provider-settlement.dto.ts b/src/admin/dto/provider-settlement.dto.ts index 9c69e39..e0400de 100644 --- a/src/admin/dto/provider-settlement.dto.ts +++ b/src/admin/dto/provider-settlement.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsNumber, IsOptional, IsString, IsUUID } from 'class-validator'; +import { IsNumber, IsOptional, IsString, IsUUID, IsUrl } from 'class-validator'; export class ProviderSettlementDto { @@ -9,7 +9,7 @@ export class ProviderSettlementDto { @ApiProperty() @IsOptional() - @IsString() + @IsUrl() imgLink?: string; @ApiProperty() diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 05288fd..7eec726 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -113,7 +113,7 @@ export class CourseService { async giveCourseFeedback(courseId: number, userId: string, feedbackDto: FeedbackDto) { // Validate course - await this.getCourse(courseId); + const course = await this.getCourse(courseId); // Find purchase record with consumer Id and course ID and throw error if not found // Or if course not complete @@ -143,6 +143,24 @@ export class CourseService { ...feedbackDto } }); + + // Change average rating of the course + const avgRating = await this.prisma.userCourse.aggregate({ + where: { + courseId + }, + _avg: { + rating: true + } + }); + await this.prisma.course.update({ + where: { + id: courseId + }, + data: { + avgRating: avgRating._avg.rating + } + }); } async deleteCourse(courseId: number) { diff --git a/src/course/dto/add-course.dto.ts b/src/course/dto/add-course.dto.ts index 611d97c..f7aec18 100644 --- a/src/course/dto/add-course.dto.ts +++ b/src/course/dto/add-course.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { CourseStatus, CourseVerificationStatus } from "@prisma/client"; -import { ArrayNotEmpty, IsArray, IsDate, IsEnum, IsInt, IsJSON, IsNotEmpty, IsNumber, IsOptional, IsString, Min } from "class-validator"; +import { ArrayNotEmpty, IsArray, IsDate, IsEnum, IsInt, IsJSON, IsNotEmpty, IsNumber, IsOptional, IsString, IsUrl, Min } from "class-validator"; import { CompetencyMap } from "src/utils/types"; export class AddCourseDto { @@ -20,13 +20,13 @@ export class AddCourseDto { // link for the course content @ApiProperty() @IsNotEmpty() - @IsString() + @IsUrl() courseLink: string; // course image @ApiProperty() @IsNotEmpty() - @IsString() + @IsUrl() imgLink: string; // number of credits required to purchase course diff --git a/src/course/dto/edit-course.dto.ts b/src/course/dto/edit-course.dto.ts index 2f25d7f..c949331 100644 --- a/src/course/dto/edit-course.dto.ts +++ b/src/course/dto/edit-course.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty, PartialType } from "@nestjs/swagger"; import { AddCourseDto } from "./add-course.dto"; -import { IsArray, IsDate, IsInt, IsNotEmpty, IsOptional, IsString, Min } from "class-validator"; +import { IsArray, IsDate, IsInt, IsNotEmpty, IsOptional, IsString, IsUrl, Min } from "class-validator"; import { CourseStatus } from "@prisma/client"; import { CompetencyMap } from "src/utils/types"; @@ -20,13 +20,13 @@ export class EditCourseDto { // link for the course content @ApiProperty() - @IsString() + @IsUrl() @IsOptional() courseLink?: string; // course image @ApiProperty() - @IsString() + @IsUrl() @IsOptional() imgLink?: string; diff --git a/src/course/dto/feedback.dto.ts b/src/course/dto/feedback.dto.ts index 8b18b8b..3e917fb 100644 --- a/src/course/dto/feedback.dto.ts +++ b/src/course/dto/feedback.dto.ts @@ -1,13 +1,13 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsInt, IsNotEmpty, IsString } from "class-validator"; +import { IsInt, IsNotEmpty, IsOptional, IsString } from "class-validator"; export class FeedbackDto { // Feedback Text @ApiProperty() - @IsNotEmpty() + @IsOptional() @IsString() - feedback: string; + feedback?: string; // Integer rating of the course @ApiProperty() diff --git a/src/provider/dto/login.dto.ts b/src/provider/dto/login.dto.ts index e71480b..a3dde16 100644 --- a/src/provider/dto/login.dto.ts +++ b/src/provider/dto/login.dto.ts @@ -1,18 +1,23 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsEmail, IsNotEmpty, IsStrongPassword } from "class-validator"; +export class CheckRegDto { + + @ApiProperty() + @IsNotEmpty() + @IsEmail() + email: string +} + export class LoginDto { - // email ID @ApiProperty() @IsNotEmpty() @IsEmail() email: string - // password @ApiProperty() @IsNotEmpty({ message: 'Password is required' }) - @IsStrongPassword() password: string } diff --git a/src/provider/dto/provider-profile-response.dto.ts b/src/provider/dto/provider-profile-response.dto.ts index 32c9999..96c860f 100644 --- a/src/provider/dto/provider-profile-response.dto.ts +++ b/src/provider/dto/provider-profile-response.dto.ts @@ -1,42 +1,61 @@ import { ApiProperty } from "@nestjs/swagger"; import { ProviderStatus } from "@prisma/client"; -import { IsEmail, IsEnum, IsOptional, IsString, IsUUID } from 'class-validator'; +import { IsEmail, IsEnum, IsObject, IsOptional as IsNotEmpty, IsString, IsUUID, IsOptional, IsUrl, IsPhoneNumber } from 'class-validator'; +import { PaymentInfo } from "src/utils/types"; export class ProviderProfileResponse { @ApiProperty({required: false}) @IsUUID() - @IsOptional() + @IsNotEmpty() id: string; @ApiProperty() - @IsOptional() + @IsNotEmpty() @IsString() name: string; @ApiProperty({format: 'email'}) @IsEmail() - @IsOptional() + @IsNotEmpty() email: string; @ApiProperty() @IsString() - @IsOptional() + @IsNotEmpty() password?: string; + // organisation name + @ApiProperty() + @IsNotEmpty() + @IsString() + orgName: string; + + // organisation logo image link + @ApiProperty() + @IsNotEmpty() + @IsUrl() + orgLogo: string; + // phone number @ApiProperty() - @IsOptional() - paymentInfo?: any; + @IsNotEmpty() + @IsPhoneNumber() + phone: string; @ApiProperty() @IsOptional() + @IsObject() + paymentInfo: PaymentInfo; + + @ApiProperty() + @IsNotEmpty() @IsEnum(ProviderStatus) status?: ProviderStatus; @ApiProperty() - @IsOptional() + @IsNotEmpty() @IsString() - rejectionReason?: string; + rejectionReason: string | null; // readonly courses: Course[]; } \ No newline at end of file diff --git a/src/provider/dto/signup.dto.ts b/src/provider/dto/signup.dto.ts index ae6e8e7..b31729a 100644 --- a/src/provider/dto/signup.dto.ts +++ b/src/provider/dto/signup.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsEmail, IsNotEmpty, IsOptional, IsString, IsStrongPassword } from "class-validator"; +import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString, IsStrongPassword, IsUrl } from "class-validator"; +import { PaymentInfo } from "src/utils/types"; export class SignupDto { @@ -29,12 +30,31 @@ export class SignupDto { }, { message: 'Password is not strong enough' }, ) - password: string + password: string; + + // organisation name + @ApiProperty() + @IsNotEmpty() + @IsString() + orgName: string; + + // organisation logo image link + @ApiProperty() + @IsNotEmpty() + @IsUrl() + orgLogo: string; + + // phone number + @ApiProperty() + @IsNotEmpty() + @IsPhoneNumber() + phone: string; // payment info @ApiProperty() @IsOptional() - paymentInfo?: any + @IsObject() + paymentInfo?: PaymentInfo } export class SignupResponseDto { diff --git a/src/provider/dto/update-profile.dto.ts b/src/provider/dto/update-profile.dto.ts index ca7f373..7adcc17 100644 --- a/src/provider/dto/update-profile.dto.ts +++ b/src/provider/dto/update-profile.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsEmail, IsJSON, IsOptional, IsString } from "class-validator"; +import { IsEmail, IsJSON, IsObject, IsOptional, IsPhoneNumber, IsString } from "class-validator"; +import { PaymentInfo } from "src/utils/types"; export class UpdateProfileDto { @@ -21,8 +22,27 @@ export class UpdateProfileDto { @IsOptional() password?: string + // organisation name + @ApiProperty() + @IsString() + @IsOptional() + orgName?: string + + // organisation logo image link + @ApiProperty() + @IsString() + @IsOptional() + orgLogo?: string + + // phone number + @ApiProperty() + @IsPhoneNumber() + @IsOptional() + phone?: string + // payment info @ApiProperty() @IsOptional() - paymentInfo?: any + @IsObject() + paymentInfo?: PaymentInfo } diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index 63798a4..e2aee31 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -2,7 +2,7 @@ import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, ParseIntPipe, import { ProviderService } from './provider.service'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SignupDto, SignupResponseDto } from './dto/signup.dto'; -import { LoginDto, LoginResponseDto } from './dto/login.dto'; +import { CheckRegDto, LoginDto, LoginResponseDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { AddCourseDto, AddCourseResponseDto } from 'src/course/dto/add-course.dto'; import { FeedbackResponseDto } from './dto/feedback.dto'; @@ -23,6 +23,34 @@ export class ProviderController { private providerService: ProviderService, ) {} + @ApiOperation({ summary: 'Check if provider is registered' }) + @ApiResponse({ status: HttpStatus.OK }) + @Post() + // Check if provider is registered + async checkProviderReg( + @Body() checkRegDto: CheckRegDto, + @Res() res + ) { + try { + this.logger.log(`Checking provider email`); + + await this.providerService.getProviderFromEmail(checkRegDto.email); + + this.logger.log(`Successfully found provider`); + + res.status(HttpStatus.OK).json({ + message: "Provider found" + }) + } catch (err) { + this.logger.error(`Failed to check provider`); + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to check provider", + }); + } + } + @ApiOperation({ summary: 'create provider account' }) @ApiResponse({ status: HttpStatus.CREATED, type: SignupResponseDto }) @Post("/signup") diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index b175e3d..142948d 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable, NotAcceptableException, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, HttpException, Injectable, NotAcceptableException, NotFoundException, UnauthorizedException } from '@nestjs/common'; import { SignupDto } from './dto/signup.dto'; import { PrismaService } from 'src/prisma/prisma.service'; import { ProviderStatus } from '@prisma/client'; @@ -46,33 +46,39 @@ export class ProviderService { name: signupDto.name, email: signupDto.email, password: hashedPassword, - paymentInfo: signupDto.paymentInfo ? signupDto.paymentInfo : null - // other user profile data + paymentInfo: signupDto.paymentInfo, + orgName: signupDto.orgName, + orgLogo: signupDto.orgLogo, + phone: signupDto.phone } }); - - // Forward to wallet service for creation of wallet - const url = process.env.WALLET_SERVICE_URL; - const endpoint = url + `/api/wallet/create`; - const reqBody = { - userId: provider.id, - type: 'PROVIDER' + try { + // Forward to wallet service for creation of wallet + if(!process.env.WALLET_SERVICE_URL) + throw new HttpException("Wallet service URL not defined", 500); + const url = process.env.WALLET_SERVICE_URL; + const endpoint = url + `/api/wallet/create`; + const reqBody = { + userId: provider.id, + type: 'PROVIDER', + credits: 0 + } + const resp = await axios.post(endpoint, reqBody); + } catch(err) { + await this.prisma.provider.delete({ + where: { + id: provider.id + } + }); + throw new HttpException(err.response, err.response.status); } - const resp = await axios.post(endpoint, reqBody); - return provider.id } async getProviderIdFromLogin(loginDto: LoginDto) { // Fetch the provider from email ID - const provider = await this.prisma.provider.findUnique({ - where: { - email: loginDto.email - } - }) - if(!provider) - throw new NotFoundException("Email ID does not exist"); + const provider = await this.getProviderFromEmail(loginDto.email); // Compare the entered password with the password fetched from database const isPasswordValid = await this.authService.comparePasswords(loginDto.password, provider.password); @@ -97,6 +103,20 @@ export class ProviderService { return provider; } + async getProviderFromEmail(email: string) { + + // Fetch provider details using email ID + const provider = await this.prisma.provider.findUnique({ + where: { + email + } + }); + if(!provider) + throw new NotFoundException("provider not found"); + + return provider; + } + // Used when provider makes a request to update profile async updateProfileInfo(providerId: string, updateProfileDto: UpdateProfileDto) { @@ -218,9 +238,21 @@ export class ProviderService { async fetchAllProviders(): Promise { - return this.prisma.provider.findMany({ - select: { id: true, name: true, email: true, paymentInfo: true, courses: true, status: true} - }); + const providers = await this.prisma.provider.findMany(); + + return providers.map((p) => { + return { + id: p.id, + name: p.name, + email: p.email, + paymentInfo: (typeof p.paymentInfo === "string") ? JSON.parse(p.paymentInfo) : p.paymentInfo ?? undefined, + rejectionReason: p.rejectionReason, + status: p.status, + orgLogo: p.orgLogo, + orgName: p.orgName, + phone: p.phone, + } + }) } async verifyProvider(providerId: string) { diff --git a/src/utils/types.ts b/src/utils/types.ts index 6498807..012b667 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,2 +1,11 @@ -export type CompetencyMap = Record; \ No newline at end of file +export type CompetencyMap = Record; + +export type PaymentInfo = { + bankName: string; + branch: string; + accountNumber: string; + IFSCCode: string; + PANnumber: string; + GSTNumber: string; +} \ No newline at end of file From 1333a70ad2041bc7d0425c659231a2d3722cc0da Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 24 Nov 2023 14:19:07 +0530 Subject: [PATCH 33/88] chage course status --- src/course/course.service.ts | 23 +++++++++---- src/course/dto/add-course.dto.ts | 4 +-- src/course/dto/course-status.dto.ts | 13 ++++++++ src/course/dto/edit-course.dto.ts | 12 ++----- src/provider/dto/login.dto.ts | 4 +++ .../dto/provider-profile-response.dto.ts | 2 +- src/provider/provider.controller.ts | 33 ++++++++++--------- src/provider/provider.service.ts | 23 ++++++++----- 8 files changed, 71 insertions(+), 43 deletions(-) create mode 100644 src/course/dto/course-status.dto.ts diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 7eec726..73c5f23 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -7,6 +7,7 @@ import { CompleteCourseDto } from "./dto/completion.dto"; import { EditCourseDto } from "./dto/edit-course.dto"; import { AdminCourseResponse, CourseResponse } from "src/course/dto/course-response.dto"; import { CourseTransactionDto } from "./dto/transaction.dto"; +import { CourseStatusDto } from "./dto/course-status.dto"; @Injectable() export class CourseService { @@ -74,14 +75,19 @@ export class CourseService { }); } - async archiveCourse(courseId: number) { + async changeStatus(courseId: number, providerId: string, courseStatusDto: CourseStatusDto) { + + // Validate course + const course = await this.getCourse(courseId) + + if(course.providerId != providerId) + throw new BadRequestException("Course does not belong to the provider"); // update the course status to archived return this.prisma.course.update({ - where: { id: courseId }, - data: { status: CourseStatus.ARCHIVED } + where: { id: courseId, providerId }, + data: { status: courseStatusDto.status } }); - } async editCourse(courseId: number, editCourseDto: EditCourseDto) { @@ -173,15 +179,18 @@ export class CourseService { }) } - async getProviderCourses(providerId: string) { + async getProviderCourses(providerId: string): Promise { // Get all courses added by a single provider - return this.prisma.course.findMany({ + const courses = await this.prisma.course.findMany({ where: { providerId } }) - + return courses.map((c) => { + const {cqfScore, impactScore, ...clone} = c; + return clone; + }) } async getPurchasedUsersByCourseId(courseId: number) { diff --git a/src/course/dto/add-course.dto.ts b/src/course/dto/add-course.dto.ts index f7aec18..5cb8efc 100644 --- a/src/course/dto/add-course.dto.ts +++ b/src/course/dto/add-course.dto.ts @@ -68,9 +68,9 @@ export class AddCourseDto { // course status (archived/unarchived) @ApiProperty() - @IsNotEmpty() + @IsOptional() @IsEnum(CourseStatus) - status: CourseStatus; + status?: CourseStatus; // course availability time @ApiProperty() diff --git a/src/course/dto/course-status.dto.ts b/src/course/dto/course-status.dto.ts new file mode 100644 index 0000000..f7abdc5 --- /dev/null +++ b/src/course/dto/course-status.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { CourseStatus } from "@prisma/client"; +import { IsString, IsOptional, IsNotEmpty, IsEnum } from "class-validator"; + + +export class CourseStatusDto { + + // course status (archived/unarchived) + @ApiProperty() + @IsEnum(CourseStatus) + @IsNotEmpty() + status: CourseStatus; +} \ No newline at end of file diff --git a/src/course/dto/edit-course.dto.ts b/src/course/dto/edit-course.dto.ts index c949331..7e02d0b 100644 --- a/src/course/dto/edit-course.dto.ts +++ b/src/course/dto/edit-course.dto.ts @@ -1,7 +1,5 @@ -import { ApiProperty, PartialType } from "@nestjs/swagger"; -import { AddCourseDto } from "./add-course.dto"; -import { IsArray, IsDate, IsInt, IsNotEmpty, IsOptional, IsString, IsUrl, Min } from "class-validator"; -import { CourseStatus } from "@prisma/client"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsArray, IsDate, IsInt, IsOptional, IsString, IsUrl, Min } from "class-validator"; import { CompetencyMap } from "src/utils/types"; export class EditCourseDto { @@ -67,12 +65,6 @@ export class EditCourseDto { @IsOptional() author?: string; - // course status (archived/unarchived) - // @ApiProperty() - // @IsString() - // @IsOptional() - // status: CourseStatus; - // course availability time @ApiProperty() @IsDate() diff --git a/src/provider/dto/login.dto.ts b/src/provider/dto/login.dto.ts index a3dde16..a323958 100644 --- a/src/provider/dto/login.dto.ts +++ b/src/provider/dto/login.dto.ts @@ -9,6 +9,10 @@ export class CheckRegDto { email: string } +export class CheckRegResponseDto { + readonly found: boolean +} + export class LoginDto { @ApiProperty() diff --git a/src/provider/dto/provider-profile-response.dto.ts b/src/provider/dto/provider-profile-response.dto.ts index 96c860f..c88ad5c 100644 --- a/src/provider/dto/provider-profile-response.dto.ts +++ b/src/provider/dto/provider-profile-response.dto.ts @@ -51,7 +51,7 @@ export class ProviderProfileResponse { @ApiProperty() @IsNotEmpty() @IsEnum(ProviderStatus) - status?: ProviderStatus; + status: ProviderStatus; @ApiProperty() @IsNotEmpty() diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index e2aee31..2ab43d1 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -2,7 +2,7 @@ import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, ParseIntPipe, import { ProviderService } from './provider.service'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SignupDto, SignupResponseDto } from './dto/signup.dto'; -import { CheckRegDto, LoginDto, LoginResponseDto } from './dto/login.dto'; +import { CheckRegDto, CheckRegResponseDto, LoginDto, LoginResponseDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { AddCourseDto, AddCourseResponseDto } from 'src/course/dto/add-course.dto'; import { FeedbackResponseDto } from './dto/feedback.dto'; @@ -12,6 +12,7 @@ import { EditCourseDto } from 'src/course/dto/edit-course.dto'; import { CourseResponse } from 'src/course/dto/course-response.dto'; import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; import { getPrismaErrorStatusAndMessage } from 'src/utils/utils'; +import { CourseStatusDto } from 'src/course/dto/course-status.dto'; @Controller('provider') @ApiTags('provider') @@ -24,7 +25,7 @@ export class ProviderController { ) {} @ApiOperation({ summary: 'Check if provider is registered' }) - @ApiResponse({ status: HttpStatus.OK }) + @ApiResponse({ status: HttpStatus.OK, type: CheckRegResponseDto }) @Post() // Check if provider is registered async checkProviderReg( @@ -34,12 +35,13 @@ export class ProviderController { try { this.logger.log(`Checking provider email`); - await this.providerService.getProviderFromEmail(checkRegDto.email); + const found = await this.providerService.checkProviderFromEmail(checkRegDto.email); - this.logger.log(`Successfully found provider`); + this.logger.log(`Successfully checked provider`); res.status(HttpStatus.OK).json({ - message: "Provider found" + message: "Check successful", + data: found }) } catch (err) { this.logger.error(`Failed to check provider`); @@ -206,32 +208,33 @@ export class ProviderController { } } - @ApiOperation({ summary: 'Archive course' }) + @ApiOperation({ summary: 'change course status' }) @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:providerId/course/:courseId/archive") - // edit course information - async archiveCourse( + @Patch("/:providerId/course/:courseId/status") + // change course status (archived/unarchived) + async changeCourseStatus( @Param("providerId", ParseUUIDPipe) providerId: string, @Param("courseId", ParseIntPipe) courseId: number, + @Body() courseStatusDto: CourseStatusDto, @Res() res ) { try { - this.logger.log(`Archiving course`); + this.logger.log(`Changing course status`); - await this.providerService.archiveCourse(providerId, courseId); + await this.providerService.changeCourseStatus(providerId, courseId, courseStatusDto); - this.logger.log(`Successfully archived the course`); + this.logger.log(`Successfully changed course status`); res.status(HttpStatus.OK).json({ - message: "course archived successfully", + message: "course status changed successfully", }) } catch (err) { - this.logger.error(`Failed to archive the course`); + this.logger.error(`Failed to change course status`); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ statusCode, - message: errorMessage || "Failed to archive the course`", + message: errorMessage || "Failed to change course status`", }); } } diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 142948d..eb16fe7 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, HttpException, Injectable, NotAcceptableException, import { SignupDto } from './dto/signup.dto'; import { PrismaService } from 'src/prisma/prisma.service'; import { ProviderStatus } from '@prisma/client'; -import { LoginDto } from './dto/login.dto'; +import { CheckRegResponseDto, LoginDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; import { AddCourseDto } from 'src/course/dto/add-course.dto'; import { CourseService } from 'src/course/course.service'; @@ -15,6 +15,7 @@ import { CourseResponse } from 'src/course/dto/course-response.dto'; import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; import { AuthService } from 'src/auth/auth.service'; import axios from 'axios'; +import { CourseStatusDto } from 'src/course/dto/course-status.dto'; @Injectable() @@ -78,7 +79,13 @@ export class ProviderService { async getProviderIdFromLogin(loginDto: LoginDto) { // Fetch the provider from email ID - const provider = await this.getProviderFromEmail(loginDto.email); + const provider = await this.prisma.provider.findUnique({ + where: { + email: loginDto.email + } + }); + if(!provider) + throw new NotFoundException("provider not found"); // Compare the entered password with the password fetched from database const isPasswordValid = await this.authService.comparePasswords(loginDto.password, provider.password); @@ -103,7 +110,7 @@ export class ProviderService { return provider; } - async getProviderFromEmail(email: string) { + async checkProviderFromEmail(email: string): Promise { // Fetch provider details using email ID const provider = await this.prisma.provider.findUnique({ @@ -112,11 +119,11 @@ export class ProviderService { } }); if(!provider) - throw new NotFoundException("provider not found"); + return { found: false } - return provider; + return { found: true }; } - + // Used when provider makes a request to update profile async updateProfileInfo(providerId: string, updateProfileDto: UpdateProfileDto) { @@ -177,12 +184,12 @@ export class ProviderService { return this.courseService.editCourse(courseId, editCourseDto); } - async archiveCourse(providerId: string, courseId: number) { + async changeCourseStatus(providerId: string, courseId: number, courseStatusDto: CourseStatusDto) { // Validate provider await this.getProvider(providerId); - return this.courseService.archiveCourse(courseId); + return this.courseService.changeStatus(courseId, providerId, courseStatusDto); } async getCourseFeedbacks(providerId: string, courseId: number): Promise { From aadfd9f165a663cdc4d86586de2e0c59a567c267 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Mon, 27 Nov 2023 14:37:47 +0530 Subject: [PATCH 34/88] Bug fixes --- prisma/seed.ts | 2 ++ src/admin/admin.controller.ts | 12 +++++------ src/admin/admin.service.ts | 31 ++++++++++++++------------- src/course/course.controller.ts | 4 ++-- src/course/course.service.ts | 30 ++++++++++++++++++++------ src/course/dto/course-response.dto.ts | 6 ++++-- src/provider/provider.controller.ts | 4 ++-- src/provider/provider.service.ts | 4 ++-- 8 files changed, 58 insertions(+), 35 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 03752aa..be24740 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -188,6 +188,7 @@ async function main() { author: "Jason Frig", startDate: new Date("2023-06-01"), endDate: new Date("2023-08-01"), + avgRating: 3.9, verificationStatus: CourseVerificationStatus.ACCEPTED, cqfScore: 10, } @@ -203,6 +204,7 @@ async function main() { credits: 160, noOfLessons: 100, language: ["english", "hindi"], + avgRating: 3.5, duration: 50, competency: { "Logical Thinking": ["Level5", "Level4"], diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index fbc52e6..1ff2eab 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -4,7 +4,6 @@ import { AdminService } from './admin.service'; import { ProviderProfileResponse } from '../provider/dto/provider-profile-response.dto'; import { getPrismaErrorStatusAndMessage } from '../utils/utils'; import { EditProvider } from './dto/edit-provider.dto'; -import { CourseResponse } from '../course/dto/course-response.dto'; import { TransactionResponse } from './dto/transaction-response.dto'; import { Response } from 'express'; import { CreditRequest } from './dto/credit-request.dto'; @@ -14,6 +13,7 @@ import { CourseVerify } from 'src/course/dto/verify-course.dto'; import { ProviderVerify } from './dto/provider-verify-response.dto'; import { RejectProviderResponseDto } from './dto/reject-provider-response.dto'; import { RejectProviderRequestDto } from './dto/reject-provider-request.dto'; +import { AdminCourseResponse } from 'src/course/dto/course-response.dto'; @Controller('admin') @ApiTags('admin') @@ -220,7 +220,7 @@ export class AdminController { } @ApiOperation({ summary: "Get all the courses"}) - @ApiResponse({ status: HttpStatus.OK, type: CourseResponse, isArray: true}) + @ApiResponse({ status: HttpStatus.OK, type: AdminCourseResponse, isArray: true}) @Get('/courses/') async getAllCourses(@Res() res){ try { @@ -246,7 +246,7 @@ export class AdminController { } @ApiOperation({ summary: "Get a course, given its courseId"}) - @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) + @ApiResponse({ status: HttpStatus.OK, type: AdminCourseResponse}) @Get('/courses/:courseId') async getCourseById ( @Param("courseId", ParseIntPipe) courseId: number, @Res() res @@ -275,7 +275,7 @@ export class AdminController { @ApiOperation({ summary: "Accept course and assign a cqf_score"}) - @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) + @ApiResponse({ status: HttpStatus.OK, type: AdminCourseResponse}) @Patch('/courses/:courseId/accept') async acceptCourse ( @Param("courseId", ParseIntPipe) courseId: number, @Body() verifyBody: CourseVerify, @Res() res @@ -303,7 +303,7 @@ export class AdminController { } @ApiOperation({ summary: "Reject a course given its courseId"}) - @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) + @ApiResponse({ status: HttpStatus.OK, type: AdminCourseResponse}) @Patch('/courses/:courseId/reject') async rejectCourse ( @Param("courseId", ParseIntPipe) courseId: number, @Body() courseRejectionRequestDto: RejectProviderRequestDto, @Res() res @@ -331,7 +331,7 @@ export class AdminController { } @ApiOperation({ summary: "Remove a course given its courseId"}) - @ApiResponse({ status: HttpStatus.OK, type: CourseResponse}) + @ApiResponse({ status: HttpStatus.OK, type: AdminCourseResponse}) @Delete('/courses/:courseId') async removeCourse ( @Param("courseId", ParseIntPipe) courseId: number, @Res() res diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 3c076f8..e6b5a36 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -7,6 +7,7 @@ import { MockWalletService } from '../mock-wallet/mock-wallet.service'; import { ProviderService } from 'src/provider/provider.service'; import { CourseService } from 'src/course/course.service'; import axios from 'axios'; +import { AdminCourseResponse } from 'src/course/dto/course-response.dto'; @Injectable() export class AdminService { @@ -42,13 +43,13 @@ export class AdminService { } // fetch all courses added onto the marketplace - async findAllCourses() : Promise { + async findAllCourses() : Promise { return this.courseService.fetchAllCourses(); } // find course by Id - async findCourseById(courseId: number) { + async findCourseById(courseId: number): Promise { return this.courseService.getCourse(courseId); } @@ -74,21 +75,21 @@ export class AdminService { // get all admin-consumer transactions async getTransactions(adminId: string) { + if(!process.env.WALLET_SERVICE_URL) + throw new HttpException("Wallet service URL not defined", 500); const walletService = process.env.WALLET_SERVICE_URL; const endpoint = `/admin/${adminId}/transactions/consumers`; const url = walletService + endpoint; - try { - const response = await axios.get(url); - return response.data; - - } catch (err) { - throw new Error(`Failed to fetch data: ${err.message}`); - } + + const response = await axios.get(url); + return response.data; } // Add or remove credits to provider wallet async addOrRemoveCreditsToProvider(adminId: string, providerId: string, credits: number) { const walletService = process.env.WALLET_SERVICE_URL; + if(!walletService) + throw new HttpException("Wallet service URL not defined", 500); let endpoint: string; if(credits >= 0) { endpoint = `/admin/${adminId}/add-credits`; @@ -100,12 +101,9 @@ export class AdminService { consumerId: providerId, credits: credits }; - try { - let response = await axios.post(url, requestBody); - return response; - } catch (err) { - throw new Error(`Failed to send add/reduce credits POST request to walletService.`); - } + let response = await axios.post(url, requestBody); + return response; + } // edit provider profile information @@ -136,7 +134,10 @@ export class AdminService { // Get the number of credits in a provider wallet async getProviderWalletCredits(providerId: string) { + const url = process.env.WALLET_SERVICE_URL; + if(!url) + throw new HttpException("Wallet service URL not defined", 500); const endpoint = url + `/api/providers/${providerId}/credits`; const resp = await axios.get(endpoint); return resp.data.credits; diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index 9cc2526..658ca8a 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -54,10 +54,10 @@ export class CourseController { try { this.logger.log(`Getting information of one course`); - const course: CourseResponse = await this.courseService.getCourse(courseId); + const course = await this.courseService.getCourseByConsumer(courseId); this.logger.log(`Successfully retrieved the course`); - + res.status(HttpStatus.OK).json({ message: "fetch successful", data: course diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 73c5f23..201976c 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -2,10 +2,10 @@ import { NotFoundException, BadRequestException, Injectable, NotAcceptableExcept import { PrismaService } from "../prisma/prisma.service"; import { FeedbackDto } from "./dto/feedback.dto"; import { AddCourseDto } from "./dto/add-course.dto"; -import { Course, CourseProgressStatus, CourseStatus, CourseVerificationStatus } from "@prisma/client"; +import { CourseProgressStatus, CourseVerificationStatus } from "@prisma/client"; import { CompleteCourseDto } from "./dto/completion.dto"; import { EditCourseDto } from "./dto/edit-course.dto"; -import { AdminCourseResponse, CourseResponse } from "src/course/dto/course-response.dto"; +import { AdminCourseResponse, CourseResponse, ProviderCourseResponse } from "src/course/dto/course-response.dto"; import { CourseTransactionDto } from "./dto/transaction.dto"; import { CourseStatusDto } from "./dto/course-status.dto"; @@ -43,7 +43,10 @@ export class CourseService { }] } }); - return courses; + return courses.map((c) => { + const {cqfScore, impactScore, verificationStatus, rejectionReason, ...clone} = c; + return clone; + }); } async addCourse(providerId: string, addCourseDto: AddCourseDto) { @@ -60,7 +63,7 @@ export class CourseService { async addPurchaseRecord(courseId: number, userId: string) { // Check if course already purchased - const record = this.prisma.userCourse.findFirst({ + const record = await this.prisma.userCourse.findFirst({ where: { userId: userId, courseId: courseId } }); if(record != null) @@ -116,6 +119,21 @@ export class CourseService { return course; } + async getCourseByConsumer(courseId: number): Promise { + + // Find course by ID and throw error if not found + const course = await this.prisma.course.findUnique({ + where: { + id: courseId + } + }) + if(!course) + throw new NotFoundException("Course does not exist"); + + const {cqfScore, impactScore, verificationStatus, rejectionReason, ...clone} = course; + return clone; + } + async giveCourseFeedback(courseId: number, userId: string, feedbackDto: FeedbackDto) { // Validate course @@ -179,7 +197,7 @@ export class CourseService { }) } - async getProviderCourses(providerId: string): Promise { + async getProviderCourses(providerId: string): Promise { // Get all courses added by a single provider const courses = await this.prisma.course.findMany({ @@ -218,7 +236,7 @@ export class CourseService { }) } - async fetchAllCourses() : Promise { + async fetchAllCourses() : Promise { // Fetch all courses return this.prisma.course.findMany(); diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index e142788..9d2b537 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -1,6 +1,5 @@ import { CourseStatus, CourseVerificationStatus } from "@prisma/client"; import { JsonValue } from "@prisma/client/runtime/library"; -import { CompetencyMap } from "src/utils/types"; export class CourseResponse { @@ -20,11 +19,14 @@ export class CourseResponse { readonly status: CourseStatus; readonly startDate: Date | null; readonly endDate: Date | null; +} + +export class ProviderCourseResponse extends CourseResponse { readonly verificationStatus: CourseVerificationStatus; readonly rejectionReason: string | null; } -export class AdminCourseResponse extends CourseResponse { +export class AdminCourseResponse extends ProviderCourseResponse { readonly cqfScore: number | null; readonly impactScore: number | null; diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index 2ab43d1..40f41c5 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -9,7 +9,7 @@ import { FeedbackResponseDto } from './dto/feedback.dto'; import { CourseTransactionDto } from '../course/dto/transaction.dto'; import { CompleteCourseDto } from 'src/course/dto/completion.dto'; import { EditCourseDto } from 'src/course/dto/edit-course.dto'; -import { CourseResponse } from 'src/course/dto/course-response.dto'; +import { ProviderCourseResponse } from 'src/course/dto/course-response.dto'; import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; import { getPrismaErrorStatusAndMessage } from 'src/utils/utils'; import { CourseStatusDto } from 'src/course/dto/course-status.dto'; @@ -302,7 +302,7 @@ export class ProviderController { } @ApiOperation({ summary: 'View courses offered by self' }) - @ApiResponse({ status: HttpStatus.OK, type: [CourseResponse] }) + @ApiResponse({ status: HttpStatus.OK, type: [ProviderCourseResponse] }) @Get("/:providerId/course") // View courses offered by self async fetchProviderCourses( diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index eb16fe7..a48acf3 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -11,7 +11,7 @@ import { CourseTransactionDto } from '../course/dto/transaction.dto'; import { CompleteCourseDto } from 'src/course/dto/completion.dto'; import { EditCourseDto } from 'src/course/dto/edit-course.dto'; import { EditProvider } from 'src/admin/dto/edit-provider.dto'; -import { CourseResponse } from 'src/course/dto/course-response.dto'; +import { ProviderCourseResponse } from 'src/course/dto/course-response.dto'; import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; import { AuthService } from 'src/auth/auth.service'; import axios from 'axios'; @@ -171,7 +171,7 @@ export class ProviderService { await this.courseService.deleteCourse(courseId); } - async getCourses(providerId: string): Promise { + async getCourses(providerId: string): Promise { return this.courseService.getProviderCourses(providerId); } From bd0b8a8e3ef971014d72bc10d6449834cf57beab Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Mon, 27 Nov 2023 16:24:52 +0530 Subject: [PATCH 35/88] add course dates --- src/course/dto/add-course.dto.ts | 29 ++++++++++------------------- src/course/dto/edit-course.dto.ts | 10 ++++++++-- src/provider/provider.controller.ts | 4 ++-- src/provider/provider.service.ts | 5 +++-- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/course/dto/add-course.dto.ts b/src/course/dto/add-course.dto.ts index 5cb8efc..9d3d36e 100644 --- a/src/course/dto/add-course.dto.ts +++ b/src/course/dto/add-course.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; -import { CourseStatus, CourseVerificationStatus } from "@prisma/client"; -import { ArrayNotEmpty, IsArray, IsDate, IsEnum, IsInt, IsJSON, IsNotEmpty, IsNumber, IsOptional, IsString, IsUrl, Min } from "class-validator"; +import { CourseStatus } from "@prisma/client"; +import { ArrayNotEmpty, IsArray, IsDate, IsEnum, IsInt, IsNotEmpty, IsNumber, IsOptional, IsString, IsUrl, Min } from "class-validator"; import { CompetencyMap } from "src/utils/types"; export class AddCourseDto { @@ -72,24 +72,15 @@ export class AddCourseDto { @IsEnum(CourseStatus) status?: CourseStatus; - // course availability time + // course start date @ApiProperty() @IsDate() @IsOptional() - availabilityTime?: Date; -} - -export class AddCourseResponseDto extends AddCourseDto { - - // course ID - readonly id: number; - - // course provider ID - readonly providerId: number; - - // Average rating - readonly avgRating: number; + startDate?: Date; - // Course Verification Status (pending/accepted/rejected) - readonly verificationStatus: CourseVerificationStatus; -} \ No newline at end of file + // course end date + @ApiProperty() + @IsDate() + @IsOptional() + endDate?: Date; +} diff --git a/src/course/dto/edit-course.dto.ts b/src/course/dto/edit-course.dto.ts index 7e02d0b..8b81a6b 100644 --- a/src/course/dto/edit-course.dto.ts +++ b/src/course/dto/edit-course.dto.ts @@ -65,9 +65,15 @@ export class EditCourseDto { @IsOptional() author?: string; - // course availability time + // course start date @ApiProperty() @IsDate() @IsOptional() - availabilityTime?: Date; + startDate?: Date; + + // course end date + @ApiProperty() + @IsDate() + @IsOptional() + endDate?: Date; } \ No newline at end of file diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index 40f41c5..15cedcf 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -4,7 +4,7 @@ import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SignupDto, SignupResponseDto } from './dto/signup.dto'; import { CheckRegDto, CheckRegResponseDto, LoginDto, LoginResponseDto } from './dto/login.dto'; import { UpdateProfileDto } from './dto/update-profile.dto'; -import { AddCourseDto, AddCourseResponseDto } from 'src/course/dto/add-course.dto'; +import { AddCourseDto } from 'src/course/dto/add-course.dto'; import { FeedbackResponseDto } from './dto/feedback.dto'; import { CourseTransactionDto } from '../course/dto/transaction.dto'; import { CompleteCourseDto } from 'src/course/dto/completion.dto'; @@ -240,7 +240,7 @@ export class ProviderController { } @ApiOperation({ summary: 'add new course' }) - @ApiResponse({ status: HttpStatus.CREATED, type: AddCourseResponseDto }) + @ApiResponse({ status: HttpStatus.CREATED, type: ProviderCourseResponse }) @Post("/:providerId/course") // add new course async addCourse( diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index a48acf3..56b367e 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -144,7 +144,7 @@ export class ProviderService { }); } - async addNewCourse(providerId: string, addCourseDto: AddCourseDto) { + async addNewCourse(providerId: string, addCourseDto: AddCourseDto): Promise { // Fetch provider const provider = await this.getProvider(providerId); @@ -154,7 +154,8 @@ export class ProviderService { throw new UnauthorizedException("Provider account is not verified"); // Forward to course service - return this.courseService.addCourse(providerId, addCourseDto); + const {cqfScore, impactScore, ...clone} = await this.courseService.addCourse(providerId, addCourseDto); + return clone; } async removeCourse(providerId: string, courseId: number) { From 003198b1052660117123e4af2e09a9e39ca341f6 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Tue, 28 Nov 2023 15:22:39 +0530 Subject: [PATCH 36/88] purchase changes --- src/course/course.controller.ts | 15 ++++--- src/course/course.service.ts | 56 +++++++++++++++++++------- src/course/dto/purchase.dto.ts | 33 +++++++++++++++ src/provider/dto/update-profile.dto.ts | 6 --- 4 files changed, 85 insertions(+), 25 deletions(-) create mode 100644 src/course/dto/purchase.dto.ts diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index 658ca8a..35af038 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -4,6 +4,7 @@ import { CourseService } from "./course.service"; import { FeedbackDto } from "./dto/feedback.dto"; import { CourseResponse } from "./dto/course-response.dto"; import { getPrismaErrorStatusAndMessage } from "src/utils/utils"; +import { PurchaseDto, PurchaseResponseDto } from "./dto/purchase.dto"; @Controller('course') @ApiTags('course') @@ -74,23 +75,27 @@ export class CourseController { } @ApiOperation({ summary: 'Confirmation of user purchase of a course' }) - @ApiResponse({ status: HttpStatus.OK }) - @Post("/:courseId/purchase/:userId") + @ApiResponse({ status: HttpStatus.OK, type: PurchaseResponseDto }) + @Post("/:courseId/purchase") // Confirmation of user purchase of a course async purchaseCourse( @Param("courseId", ParseIntPipe) courseId: number, - @Param("userId", ParseUUIDPipe) userId: string, + @Body() purchaseDto: PurchaseDto, + @Res() res ) { try { this.logger.log(`Recording the user purchase of the course`); - await this.courseService.addPurchaseRecord(courseId, userId); + const transactionId = await this.courseService.addPurchaseRecord(courseId, purchaseDto); this.logger.log(`Successfully recorded the purchase`); res.status(HttpStatus.OK).json({ - message: "purchase successful" + message: "purchase successful", + data: { + walletTransactionId: transactionId + } }) } catch (err) { this.logger.error(`Failed to record the purchase`); diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 201976c..cb3e5d9 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -1,13 +1,15 @@ -import { NotFoundException, BadRequestException, Injectable, NotAcceptableException } from "@nestjs/common"; +import { NotFoundException, BadRequestException, Injectable, NotAcceptableException, HttpException } from "@nestjs/common"; import { PrismaService } from "../prisma/prisma.service"; import { FeedbackDto } from "./dto/feedback.dto"; import { AddCourseDto } from "./dto/add-course.dto"; -import { CourseProgressStatus, CourseVerificationStatus } from "@prisma/client"; +import { CourseProgressStatus, CourseStatus, CourseVerificationStatus } from "@prisma/client"; import { CompleteCourseDto } from "./dto/completion.dto"; import { EditCourseDto } from "./dto/edit-course.dto"; import { AdminCourseResponse, CourseResponse, ProviderCourseResponse } from "src/course/dto/course-response.dto"; import { CourseTransactionDto } from "./dto/transaction.dto"; import { CourseStatusDto } from "./dto/course-status.dto"; +import axios from "axios"; +import { PurchaseDto } from "./dto/purchase.dto"; @Injectable() export class CourseService { @@ -19,7 +21,7 @@ export class CourseService { // Searches for the courses available in the DB that match or contain the input search string // in their title, author, description or competency - const courses = await this.prisma.course.findMany({ + let courses = await this.prisma.course.findMany({ where: { OR: [{ title: { @@ -43,6 +45,12 @@ export class CourseService { }] } }); + courses = courses.filter((c) => + c.verificationStatus == CourseVerificationStatus.ACCEPTED + && c.status == CourseStatus.UNARCHIVED + && (c.startDate ? c.startDate <= new Date(): true) + && (c.endDate ? c.endDate <= new Date(): true) + ); return courses.map((c) => { const {cqfScore, impactScore, verificationStatus, rejectionReason, ...clone} = c; return clone; @@ -60,22 +68,41 @@ export class CourseService { }); } - async addPurchaseRecord(courseId: number, userId: string) { + async addPurchaseRecord(courseId: number, purchaseDto: PurchaseDto) { // Check if course already purchased const record = await this.prisma.userCourse.findFirst({ - where: { userId: userId, courseId: courseId } + where: { userId: purchaseDto.consumerId, courseId: courseId } }); if(record != null) throw new BadRequestException("Course already purchased by the user"); // create new record for purchase - return await this.prisma.userCourse.create({ + await this.prisma.userCourse.create({ data: { - userId, + userId: purchaseDto.consumerId, courseId, } }); + // forward to wallet service for transaction + try { + const endpoint = `/api/consumers/${purchaseDto.consumerId}/purchase`; + + const {consumerId, ...walletPurchaseBody } = purchaseDto; + const walletResponse = await axios.post(process.env.WALLET_SERVICE_URL + endpoint, walletPurchaseBody); + return walletResponse.data.data.transaction.transactionId; + } catch (err) { + // if transaction failed, delete the record + await this.prisma.userCourse.delete({ + where: { + userId_courseId: { + userId: purchaseDto.consumerId, + courseId + } + } + }); + throw new HttpException(err.response.data, err.response.status | err.status) + } } async changeStatus(courseId: number, providerId: string, courseStatusDto: CourseStatusDto) { @@ -122,13 +149,14 @@ export class CourseService { async getCourseByConsumer(courseId: number): Promise { // Find course by ID and throw error if not found - const course = await this.prisma.course.findUnique({ - where: { - id: courseId - } - }) - if(!course) - throw new NotFoundException("Course does not exist"); + const course = await this.getCourse(courseId); + + if(course.verificationStatus != CourseVerificationStatus.ACCEPTED) + throw new BadRequestException("Course is not accepted"); + if(course.status != CourseStatus.UNARCHIVED) + throw new BadRequestException("Course is archived"); + if((course.startDate && course.startDate > new Date()) || (course.endDate && course.endDate < new Date())) + throw new BadRequestException("Course is not available at the moment"); const {cqfScore, impactScore, verificationStatus, rejectionReason, ...clone} = course; return clone; diff --git a/src/course/dto/purchase.dto.ts b/src/course/dto/purchase.dto.ts new file mode 100644 index 0000000..c185cba --- /dev/null +++ b/src/course/dto/purchase.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsIn, IsInt, IsNotEmpty, IsString, IsUUID } from "class-validator"; + + +export class PurchaseDto { + // consumer ID + @ApiProperty() + @IsNotEmpty() + @IsUUID() + consumerId: string; + + // provider ID + @ApiProperty() + @IsNotEmpty() + @IsUUID() + providerId: string; + + // Number of credits transferred + @ApiProperty() + @IsNotEmpty() + @IsInt() + credits: number; + + // Purchase description + @ApiProperty() + @IsNotEmpty() + @IsString() + description: string; +} + +export class PurchaseResponseDto { + readonly walletTransactionId: number; +} \ No newline at end of file diff --git a/src/provider/dto/update-profile.dto.ts b/src/provider/dto/update-profile.dto.ts index 7adcc17..dc3f7fa 100644 --- a/src/provider/dto/update-profile.dto.ts +++ b/src/provider/dto/update-profile.dto.ts @@ -16,12 +16,6 @@ export class UpdateProfileDto { @IsOptional() email?: string - // password - @ApiProperty() - @IsString() - @IsOptional() - password?: string - // organisation name @ApiProperty() @IsString() From b334dca58818eeeef28ae1b1382db84f8ccc18d0 Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Tue, 28 Nov 2023 15:22:41 +0530 Subject: [PATCH 37/88] Added course filter --- src/course/course.controller.ts | 31 +++++++++++++++++++++++++++++ src/course/course.service.ts | 18 +++++++++++++++++ src/course/dto/filter-course.dto.ts | 13 ++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 src/course/dto/filter-course.dto.ts diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index 9cc2526..da485c5 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -4,6 +4,7 @@ import { CourseService } from "./course.service"; import { FeedbackDto } from "./dto/feedback.dto"; import { CourseResponse } from "./dto/course-response.dto"; import { getPrismaErrorStatusAndMessage } from "src/utils/utils"; +import { FilterCourseDTO } from "./dto/filter-course.dto"; @Controller('course') @ApiTags('course') @@ -134,4 +135,34 @@ export class CourseController { } } + @ApiOperation({ summary: 'Filter for verified Courses' }) + @ApiResponse({ status: HttpStatus.OK }) + @Post("/verifyFilter") + // Filter for admin verified courses + async verifiedFilter( + @Body() courses: FilterCourseDTO[], + @Res() res + ) { + try { + this.logger.log(`Filtering for courses verified by admin`); + + const filteredCourses: FilterCourseDTO[] = await this.courseService.filterVerified(courses); + + this.logger.log(`Successfully filtered the courses`); + + res.status(HttpStatus.OK).json({ + message: "Filtering successfull", + data: filteredCourses + }); + } catch (err) { + this.logger.error(`Failed to filter the courses`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to filter the courses", + }); + } + } + } diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 05288fd..69d8ddc 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -7,6 +7,7 @@ import { CompleteCourseDto } from "./dto/completion.dto"; import { EditCourseDto } from "./dto/edit-course.dto"; import { AdminCourseResponse, CourseResponse } from "src/course/dto/course-response.dto"; import { CourseTransactionDto } from "./dto/transaction.dto"; +import { FilterCourseDTO } from "./dto/filter-course.dto"; @Injectable() export class CourseService { @@ -280,4 +281,21 @@ export class CourseService { } }); } + + async filterVerified(courses: FilterCourseDTO[]) { + + return courses.filter(async (course) => { + const exists = await this.prisma.course.count({ + where: { + provider: { + name: course.provider_name + }, + title: course.title, + verificationStatus: CourseVerificationStatus.ACCEPTED + } + }); + return exists !== 0; + }); + + } } diff --git a/src/course/dto/filter-course.dto.ts b/src/course/dto/filter-course.dto.ts new file mode 100644 index 0000000..5f1a2a6 --- /dev/null +++ b/src/course/dto/filter-course.dto.ts @@ -0,0 +1,13 @@ +export class FilterCourseDTO { + readonly id: string; + readonly title: string; + readonly long_desc: string; + readonly provider_name: string; + readonly provider_id: string; + readonly price: string; + readonly languages: string[]; + readonly imgUrl: string; + readonly rating: string; + readonly duration: string; + readonly noOfPurchases: number; +} \ No newline at end of file From a0d2a410ec7db5b8e16ddde034c5731a0aa1dc87 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Wed, 29 Nov 2023 01:12:54 +0530 Subject: [PATCH 38/88] settlement updates --- src/admin/admin.controller.ts | 11 +++--- src/admin/admin.service.ts | 44 ++++++++++++------------ src/admin/dto/provider-settlement.dto.ts | 2 +- src/course/course.service.ts | 9 +++-- src/course/dto/purchase.dto.ts | 8 ++++- src/provider/provider.service.ts | 39 +++++++++++++++++++++ 6 files changed, 82 insertions(+), 31 deletions(-) diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 1ff2eab..c0efadf 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -50,12 +50,15 @@ export class AdminController { @ApiOperation({ summary: "Get all providers for settlement" }) @ApiResponse({ status: HttpStatus.OK, type: ProviderSettlementDto, isArray: true}) - @Get('/providers/settlements') - async getAllProvidersForSettlement(@Res() res : Response) { + @Get('/:adminId/providers/settlements') + async getAllProvidersForSettlement( + @Param("adminId", ParseUUIDPipe) adminId: string, + @Res() res : Response + ) { try { this.logger.log(`Getting information of all the providers for settlement`); - const providers = await this.adminService.getAllProviderInfoForSettlement(); + const providers = await this.adminService.getAllProviderInfoForSettlement(adminId); this.logger.log(`Successfully retrieved all the provider info for making settlement`); @@ -76,7 +79,7 @@ export class AdminController { @ApiOperation({ summary: "Settle credits for a provider" }) @ApiResponse({ status: HttpStatus.OK, type: json}) - @Post('/:adminId/providers/settlements/settle') + @Post('/:adminId/providers/settlements') async settleProvider(@Param("adminId", ParseUUIDPipe) adminId: string, @Body() settleDto: ProviderSettlementDto, @Res() res : Response) { try { this.logger.log(`Settling the credits for the given provider`); diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index e6b5a36..ea5f2c4 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -8,6 +8,7 @@ import { ProviderService } from 'src/provider/provider.service'; import { CourseService } from 'src/course/course.service'; import axios from 'axios'; import { AdminCourseResponse } from 'src/course/dto/course-response.dto'; +import { ProviderSettlementDto } from './dto/provider-settlement.dto'; @Injectable() export class AdminService { @@ -133,46 +134,45 @@ export class AdminService { } // Get the number of credits in a provider wallet - async getProviderWalletCredits(providerId: string) { + async getAllProvidersWalletCredits(adminId: string) { const url = process.env.WALLET_SERVICE_URL; if(!url) throw new HttpException("Wallet service URL not defined", 500); - const endpoint = url + `/api/providers/${providerId}/credits`; + const endpoint = url + `/api/admin/${adminId}/credits/providers`; const resp = await axios.get(endpoint); - return resp.data.credits; + const creditsResponse = resp.data.data.credits; + const creditsMap = {}; + creditsResponse.forEach((c) => { + creditsMap[c.providerId] = c.credits; + }); + return creditsMap; } // Get all the providers information for settlement - async getAllProviderInfoForSettlement() { + async getAllProviderInfoForSettlement(adminId: string): Promise { - const providers = await this.providerService.fetchAllProviders(); - const results = providers.map(async (provider) => { - const providerId = provider.id; + const providers = await this.providerService.fetchProvidersForSettlement(); + const creditsMap = await this.getAllProvidersWalletCredits(adminId); + + return providers.map((p) => { return { - id: providerId, - name: provider.name, - numberOfCourses: await this.getNumberOfCoursesForProvider(providerId), - activeUsers: await this.getNoOfCoursePurchasesForProvider(providerId), - totalCredits: await this.getProviderWalletCredits(providerId) + ...p, + totalCredits: creditsMap[p.id] } }); - return results; } // settle credits for a provider async settleCredits(adminId: string, providerId: string) { // Need to add transaction, add paymentReceipt additional settlement processing - // then set the credits of the provider to 0 - const totCredits = await this.getProviderWalletCredits(providerId); + const url = process.env.WALLET_SERVICE_URL; - const endpoint = url + `/api/providers/${providerId}/settlement-transaction`; - const reqBody = { - adminId: adminId, - credits: totCredits - }; - const response = await axios.post(endpoint, reqBody); - return; + if(!url) + throw new HttpException("Wallet service URL not defined", 500); + const endpoint = url + `/api/admin/${adminId}/providers/${providerId}/settle-credits`; + + const response = await axios.post(endpoint); } } diff --git a/src/admin/dto/provider-settlement.dto.ts b/src/admin/dto/provider-settlement.dto.ts index e0400de..7d587ed 100644 --- a/src/admin/dto/provider-settlement.dto.ts +++ b/src/admin/dto/provider-settlement.dto.ts @@ -3,7 +3,7 @@ import { IsNumber, IsOptional, IsString, IsUUID, IsUrl } from 'class-validator'; export class ProviderSettlementDto { - @ApiProperty({required: false}) + @ApiProperty() @IsUUID() id: string; diff --git a/src/course/course.service.ts b/src/course/course.service.ts index cb3e5d9..6a1ac26 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -9,7 +9,7 @@ import { AdminCourseResponse, CourseResponse, ProviderCourseResponse } from "src import { CourseTransactionDto } from "./dto/transaction.dto"; import { CourseStatusDto } from "./dto/course-status.dto"; import axios from "axios"; -import { PurchaseDto } from "./dto/purchase.dto"; +import { PurchaseDto, WalletPurchaseDto } from "./dto/purchase.dto"; @Injectable() export class CourseService { @@ -87,8 +87,11 @@ export class CourseService { // forward to wallet service for transaction try { const endpoint = `/api/consumers/${purchaseDto.consumerId}/purchase`; - - const {consumerId, ...walletPurchaseBody } = purchaseDto; + const walletPurchaseBody: WalletPurchaseDto = { + providerId: purchaseDto.providerId, + credits: purchaseDto.credits, + description: purchaseDto.transactionDescription + } const walletResponse = await axios.post(process.env.WALLET_SERVICE_URL + endpoint, walletPurchaseBody); return walletResponse.data.data.transaction.transactionId; } catch (err) { diff --git a/src/course/dto/purchase.dto.ts b/src/course/dto/purchase.dto.ts index c185cba..aafb5ef 100644 --- a/src/course/dto/purchase.dto.ts +++ b/src/course/dto/purchase.dto.ts @@ -25,9 +25,15 @@ export class PurchaseDto { @ApiProperty() @IsNotEmpty() @IsString() - description: string; + transactionDescription: string; } export class PurchaseResponseDto { readonly walletTransactionId: number; +} + +export class WalletPurchaseDto { + readonly providerId: string; + readonly credits: number; + readonly description: string; } \ No newline at end of file diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 56b367e..3e3bdaa 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -16,6 +16,7 @@ import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; import { AuthService } from 'src/auth/auth.service'; import axios from 'axios'; import { CourseStatusDto } from 'src/course/dto/course-status.dto'; +import { ProviderSettlementDto } from 'src/admin/dto/provider-settlement.dto'; @Injectable() @@ -263,6 +264,44 @@ export class ProviderService { }) } + async fetchProvidersForSettlement(): Promise { + + const providers = await this.prisma.provider.findMany({ + select: { + id: true, + orgLogo: true, + orgName: true, + courses: { + select: { + userCourses: { + select: { + userId: true + } + } + }, + } + } + }); + const results = providers.map(async (provider): Promise => { + const providerId = provider.id; + const courses = provider.courses; + const activeUsers = new Set(); + courses.forEach((c) => { + c.userCourses.forEach((uc) => { + activeUsers.add(uc.userId); + }) + }) + return { + id: providerId, + name: provider.orgName, + imgLink: provider.orgLogo, + totalCourses: courses.length, + activeUsers: activeUsers.size + } + }) + return Promise.all(results); + } + async verifyProvider(providerId: string) { // Fetch provider From 327dad253b480b5c3bb6e13ee47eda713fda4ea1 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Wed, 29 Nov 2023 01:35:24 +0530 Subject: [PATCH 39/88] seed update --- prisma/seed.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/prisma/seed.ts b/prisma/seed.ts index be24740..9efedde 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -5,6 +5,7 @@ async function main() { const response = await prisma.provider.create({ data: { + id: "123e4567-e89b-42d3-a456-556642440010", name: "Vijay Salgaonkar", email: "vijaysalgaonkar@gmail.com", password: "9d209aacaed4088d68c41bd8dfb20de39cbd8339", @@ -17,6 +18,7 @@ async function main() { const provider1 = await prisma.provider.create({ data: { + id: "123e4567-e89b-42d3-a456-556642440011", name: "udemy", email: "udemyorg@gmail.in", password: "Udemy@9812", @@ -35,6 +37,7 @@ async function main() { const provider2 = await prisma.provider.create({ data: { + id: "123e4567-e89b-42d3-a456-556642440012", name: "coursera", email: "coursera@gmail.in", password: "Coursera@999", @@ -53,6 +56,7 @@ async function main() { const provider3 = await prisma.provider.create({ data: { + id: "123e4567-e89b-42d3-a456-556642440013", name: "lern", email: "lern@gmail.in", password: "lern@999", From c6f0a4ebc5fa20b87dd48f0ebbfb3453e9dd0559 Mon Sep 17 00:00:00 2001 From: Prashant Kumar <144701828+prashantesmagico@users.noreply.github.com> Date: Wed, 29 Nov 2023 01:43:34 +0530 Subject: [PATCH 40/88] Added API for the reset password (#10) * Added API for the reset password * Minor changes --- src/provider/dto/update-password.dto.ts | 16 + src/provider/provider.controller.ts | 820 +++++++++++++----------- src/provider/provider.service.ts | 556 ++++++++-------- 3 files changed, 760 insertions(+), 632 deletions(-) create mode 100644 src/provider/dto/update-password.dto.ts diff --git a/src/provider/dto/update-password.dto.ts b/src/provider/dto/update-password.dto.ts new file mode 100644 index 0000000..754f357 --- /dev/null +++ b/src/provider/dto/update-password.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsStrongPassword } from "class-validator"; + +export class UpdatePasswordDto { + // old password + @ApiProperty() + @IsNotEmpty({ message: "Old Password is required" }) + @IsStrongPassword() + oldPassword: string; + + // new password + @ApiProperty() + @IsNotEmpty({ message: "New Password is required" }) + @IsStrongPassword() + newPassword: string; +} \ No newline at end of file diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index 63798a4..9e60a70 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -1,393 +1,453 @@ -import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Put, Res } from '@nestjs/common'; -import { ProviderService } from './provider.service'; -import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { SignupDto, SignupResponseDto } from './dto/signup.dto'; -import { LoginDto, LoginResponseDto } from './dto/login.dto'; -import { UpdateProfileDto } from './dto/update-profile.dto'; -import { AddCourseDto, AddCourseResponseDto } from 'src/course/dto/add-course.dto'; -import { FeedbackResponseDto } from './dto/feedback.dto'; -import { CourseTransactionDto } from '../course/dto/transaction.dto'; -import { CompleteCourseDto } from 'src/course/dto/completion.dto'; -import { EditCourseDto } from 'src/course/dto/edit-course.dto'; -import { CourseResponse } from 'src/course/dto/course-response.dto'; -import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; -import { getPrismaErrorStatusAndMessage } from 'src/utils/utils'; - -@Controller('provider') -@ApiTags('provider') +import { + Body, + Controller, + Delete, + Get, + HttpStatus, + Logger, + Param, + ParseIntPipe, + ParseUUIDPipe, + Patch, + Post, + Put, + Res, +} from "@nestjs/common"; +import { ProviderService } from "./provider.service"; +import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; +import { SignupDto, SignupResponseDto } from "./dto/signup.dto"; +import { LoginDto, LoginResponseDto } from "./dto/login.dto"; +import { UpdateProfileDto } from "./dto/update-profile.dto"; +import { + AddCourseDto, + AddCourseResponseDto, +} from "src/course/dto/add-course.dto"; +import { FeedbackResponseDto } from "./dto/feedback.dto"; +import { CourseTransactionDto } from "../course/dto/transaction.dto"; +import { CompleteCourseDto } from "src/course/dto/completion.dto"; +import { EditCourseDto } from "src/course/dto/edit-course.dto"; +import { CourseResponse } from "src/course/dto/course-response.dto"; +import { ProviderProfileResponse } from "./dto/provider-profile-response.dto"; +import { getPrismaErrorStatusAndMessage } from "src/utils/utils"; +import { UpdatePasswordDto } from "./dto/update-password.dto"; + +@Controller("provider") +@ApiTags("provider") export class ProviderController { - - private readonly logger = new Logger(ProviderController.name); - - constructor( - private providerService: ProviderService, - ) {} - - @ApiOperation({ summary: 'create provider account' }) - @ApiResponse({ status: HttpStatus.CREATED, type: SignupResponseDto }) - @Post("/signup") - // create a new provider account - async createAccount( - @Body() signupDto: SignupDto, - @Res() res - ) { - try { - this.logger.log(`Creating new provider account`); - - const providerId = await this.providerService.createNewAccount(signupDto); - - this.logger.log(`successfully created new provider account`); - - res.status(HttpStatus.CREATED).json({ - message: "account created successfully", - data: { - providerId - } - }) - } catch (err) { - this.logger.error(`Failed to create new provider account`); - - const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to create new provider account`", - }); - } + private readonly logger = new Logger(ProviderController.name); + + constructor(private providerService: ProviderService) {} + + @ApiOperation({ summary: "create provider account" }) + @ApiResponse({ status: HttpStatus.CREATED, type: SignupResponseDto }) + @Post("/signup") + // create a new provider account + async createAccount(@Body() signupDto: SignupDto, @Res() res) { + try { + this.logger.log(`Creating new provider account`); + + const providerId = await this.providerService.createNewAccount(signupDto); + + this.logger.log(`successfully created new provider account`); + + res.status(HttpStatus.CREATED).json({ + message: "account created successfully", + data: { + providerId, + }, + }); + } catch (err) { + this.logger.error(`Failed to create new provider account`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to create new provider account`", + }); } - - @ApiOperation({ summary: 'provider login' }) - @ApiResponse({ status: HttpStatus.OK, type: LoginResponseDto }) - @Post("/login") - // provider login - async login( - @Body() loginDto: LoginDto, - @Res() res - ) { - try { - this.logger.log(`Getting provider ID`); - - const providerId = await this.providerService.getProviderIdFromLogin(loginDto); - - this.logger.log(`successfully logged in`); - - res.status(HttpStatus.OK).json({ - message: "login successful", - data: { - providerId - } - }) - } catch (err) { - this.logger.error(`Failed to log in`); - - const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to log in`", - }); - } + } + + @ApiOperation({ summary: "provider login" }) + @ApiResponse({ status: HttpStatus.OK, type: LoginResponseDto }) + @Post("/login") + // provider login + async login(@Body() loginDto: LoginDto, @Res() res) { + try { + this.logger.log(`Getting provider ID`); + + const providerId = await this.providerService.getProviderIdFromLogin( + loginDto + ); + + this.logger.log(`successfully logged in`); + + res.status(HttpStatus.OK).json({ + message: "login successful", + data: { + providerId, + }, + }); + } catch (err) { + this.logger.error(`Failed to log in`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to log in`", + }); } - - @ApiOperation({ summary: 'view provider profile' }) - @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse }) - @Get("/:providerId/profile") - // view provider profile information - async viewProfile( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Res() res - ) { - try { - this.logger.log(`Getting provider profile`); - - const provider = await this.providerService.getProvider(providerId); - - this.logger.log(`successfully retreived provider profile`); - - res.status(HttpStatus.OK).json({ - message: "fetch successful", - data : provider - }) - } catch (err) { - this.logger.error(`Failed to retreive provider profile`); - - const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to retreive provider profile`", - }); - } + } + + @ApiOperation({ summary: "view provider profile" }) + @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse }) + @Get("/:providerId/profile") + // view provider profile information + async viewProfile( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Res() res + ) { + try { + this.logger.log(`Getting provider profile`); + + const provider = await this.providerService.getProvider(providerId); + + this.logger.log(`successfully retreived provider profile`); + + res.status(HttpStatus.OK).json({ + message: "fetch successful", + data: provider, + }); + } catch (err) { + this.logger.error(`Failed to retreive provider profile`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to retreive provider profile`", + }); } - - @ApiOperation({ summary: 'update provider profile information' }) - @ApiResponse({ status: HttpStatus.OK }) - @Put("/:providerId/profile") - // update provider profile information - async updateProfile( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Body() updateProfileDto: UpdateProfileDto, - @Res() res - ) { - try { - this.logger.log(`Updating provider profile`); - - await this.providerService.updateProfileInfo(providerId, updateProfileDto); - - this.logger.log(`successfully updated provider profile`); - - res.status(HttpStatus.OK).json({ - message: "account updated successfully", - }) - } catch (err) { - this.logger.error(`Failed to update provider profile`); - - const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to update provider profile`", - }); - } + } + + @ApiOperation({ summary: "update provider profile information" }) + @ApiResponse({ status: HttpStatus.OK }) + @Put("/:providerId/profile") + // update provider profile information + async updateProfile( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Body() updateProfileDto: UpdateProfileDto, + @Res() res + ) { + try { + this.logger.log(`Updating provider profile`); + + await this.providerService.updateProfileInfo( + providerId, + updateProfileDto + ); + + this.logger.log(`successfully updated provider profile`); + + res.status(HttpStatus.OK).json({ + message: "account updated successfully", + }); + } catch (err) { + this.logger.error(`Failed to update provider profile`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to update provider profile`", + }); } - - @ApiOperation({ summary: 'edit course information' }) - @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:providerId/course/:courseId") - // edit course information - async editCourse( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, - @Body() editCourseDto: EditCourseDto, - @Res() res - ) { - try { - this.logger.log(`Updating course information`); - - await this.providerService.editCourse(providerId, courseId, editCourseDto); - - this.logger.log(`Successfully updated course information`); - - res.status(HttpStatus.OK).json({ - message: "course edited successfully", - }) - } catch (err) { - this.logger.error(`Failed to update course information`); - - const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to update course information`", - }); - } + } + + @ApiOperation({ summary: "edit course information" }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:providerId/course/:courseId") + // edit course information + async editCourse( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Param("courseId", ParseIntPipe) courseId: number, + @Body() editCourseDto: EditCourseDto, + @Res() res + ) { + try { + this.logger.log(`Updating course information`); + + await this.providerService.editCourse( + providerId, + courseId, + editCourseDto + ); + + this.logger.log(`Successfully updated course information`); + + res.status(HttpStatus.OK).json({ + message: "course edited successfully", + }); + } catch (err) { + this.logger.error(`Failed to update course information`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to update course information`", + }); } - - @ApiOperation({ summary: 'Archive course' }) - @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:providerId/course/:courseId/archive") - // edit course information - async archiveCourse( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, - @Res() res - ) { - try { - this.logger.log(`Archiving course`); - - await this.providerService.archiveCourse(providerId, courseId); - - this.logger.log(`Successfully archived the course`); - - res.status(HttpStatus.OK).json({ - message: "course archived successfully", - }) - } catch (err) { - this.logger.error(`Failed to archive the course`); - - const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to archive the course`", - }); - } + } + + @ApiOperation({ summary: "Archive course" }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:providerId/course/:courseId/archive") + // edit course information + async archiveCourse( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Param("courseId", ParseIntPipe) courseId: number, + @Res() res + ) { + try { + this.logger.log(`Archiving course`); + + await this.providerService.archiveCourse(providerId, courseId); + + this.logger.log(`Successfully archived the course`); + + res.status(HttpStatus.OK).json({ + message: "course archived successfully", + }); + } catch (err) { + this.logger.error(`Failed to archive the course`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to archive the course`", + }); } - - @ApiOperation({ summary: 'add new course' }) - @ApiResponse({ status: HttpStatus.CREATED, type: AddCourseResponseDto }) - @Post("/:providerId/course") - // add new course - async addCourse( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Body() addCourseDto: AddCourseDto, - @Res() res - ) { - try { - this.logger.log(`Adding new course`); - - const course = await this.providerService.addNewCourse(providerId, addCourseDto); - - this.logger.log(`Successfully added new course`); - - res.status(HttpStatus.CREATED).json({ - message: "course added successfully", - data: course - }) - } catch (err) { - this.logger.error(`Failed to add the course`); - - const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to add the course`", - }); - } + } + + @ApiOperation({ summary: "add new course" }) + @ApiResponse({ status: HttpStatus.CREATED, type: AddCourseResponseDto }) + @Post("/:providerId/course") + // add new course + async addCourse( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Body() addCourseDto: AddCourseDto, + @Res() res + ) { + try { + this.logger.log(`Adding new course`); + + const course = await this.providerService.addNewCourse( + providerId, + addCourseDto + ); + + this.logger.log(`Successfully added new course`); + + res.status(HttpStatus.CREATED).json({ + message: "course added successfully", + data: course, + }); + } catch (err) { + this.logger.error(`Failed to add the course`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to add the course`", + }); } - - @ApiOperation({ summary: 'remove a course' }) - @ApiResponse({ status: HttpStatus.OK }) - @Delete("/:providerId/course/:courseId") - // remove an existing course - async removeCourse( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, - @Res() res - ) { - try { - this.logger.log(`Removing course`); - - await this.providerService.removeCourse(providerId, courseId); - - this.logger.log(`Successfully deleted the course`); - - - res.status(HttpStatus.OK).json({ - message: "course deleted successfully", - }) - } catch (err) { - this.logger.error(`Failed to delete the course`); - - const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to delete the course`", - }); - } + } + + @ApiOperation({ summary: "remove a course" }) + @ApiResponse({ status: HttpStatus.OK }) + @Delete("/:providerId/course/:courseId") + // remove an existing course + async removeCourse( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Param("courseId", ParseIntPipe) courseId: number, + @Res() res + ) { + try { + this.logger.log(`Removing course`); + + await this.providerService.removeCourse(providerId, courseId); + + this.logger.log(`Successfully deleted the course`); + + res.status(HttpStatus.OK).json({ + message: "course deleted successfully", + }); + } catch (err) { + this.logger.error(`Failed to delete the course`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to delete the course`", + }); } - - @ApiOperation({ summary: 'View courses offered by self' }) - @ApiResponse({ status: HttpStatus.OK, type: [CourseResponse] }) - @Get("/:providerId/course") - // View courses offered by self - async fetchProviderCourses( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Res() res - ) { - try { - this.logger.log(`Getting courses`); - - const courses = await this.providerService.getCourses(providerId); - - this.logger.log(`Successfully retrieved the courses`); - - res.status(HttpStatus.OK).json({ - message: "courses fetched successfully", - data: courses - }) - } catch (err) { - this.logger.error(`Failed to fetch the courses`); - - const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to fetch the courses", - }); - } + } + + @ApiOperation({ summary: "View courses offered by self" }) + @ApiResponse({ status: HttpStatus.OK, type: [CourseResponse] }) + @Get("/:providerId/course") + // View courses offered by self + async fetchProviderCourses( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Res() res + ) { + try { + this.logger.log(`Getting courses`); + + const courses = await this.providerService.getCourses(providerId); + + this.logger.log(`Successfully retrieved the courses`); + + res.status(HttpStatus.OK).json({ + message: "courses fetched successfully", + data: courses, + }); + } catch (err) { + this.logger.error(`Failed to fetch the courses`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the courses", + }); } - - @ApiOperation({ summary: 'View Course Feedback & ratings, numberOfPurchases' }) - @ApiResponse({ status: HttpStatus.OK, type: FeedbackResponseDto }) - @Get("/:providerId/course/:courseId/feedback") - // View Course Feedback & ratings, numberOfPurchases - async getCourseFeedback( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, - @Res() res - ) { - try { - this.logger.log(`Getting course feedbacks`); - - const feedbackResponse = await this.providerService.getCourseFeedbacks(providerId, courseId); - - this.logger.log(`Successfully retrieved the feedbacks`); - - res.status(HttpStatus.OK).json({ - message: "feedbacks fetched successfully", - data: feedbackResponse - }) - } catch (err) { - this.logger.error(`Failed to fetch the feedbacks`); - - const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to fetch the feedbacks", - }); - } + } + + @ApiOperation({ + summary: "View Course Feedback & ratings, numberOfPurchases", + }) + @ApiResponse({ status: HttpStatus.OK, type: FeedbackResponseDto }) + @Get("/:providerId/course/:courseId/feedback") + // View Course Feedback & ratings, numberOfPurchases + async getCourseFeedback( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Param("courseId", ParseIntPipe) courseId: number, + @Res() res + ) { + try { + this.logger.log(`Getting course feedbacks`); + + const feedbackResponse = await this.providerService.getCourseFeedbacks( + providerId, + courseId + ); + + this.logger.log(`Successfully retrieved the feedbacks`); + + res.status(HttpStatus.OK).json({ + message: "feedbacks fetched successfully", + data: feedbackResponse, + }); + } catch (err) { + this.logger.error(`Failed to fetch the feedbacks`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the feedbacks", + }); } - - @ApiOperation({ summary: 'Get transactions of all courses' }) - @ApiResponse({ status: HttpStatus.OK, type: [CourseTransactionDto] }) - @Get("/:providerId/course/transactions") - // Get transactions of all courses - async getCourseTransactions( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Res() res - ) { - try { - this.logger.log(`Getting course transactions`); - - const transactionsResponse = await this.providerService.getCourseTransactions(providerId); - - this.logger.log(`Successfully retrieved course transactions`); - - res.status(HttpStatus.OK).json({ - message: "transactions fetched successfully", - data: transactionsResponse - }) - } catch (err) { - this.logger.error(`Failed to fetch the transactions`); - - const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to fetch the transactions", - }); - } + } + + @ApiOperation({ summary: "Get transactions of all courses" }) + @ApiResponse({ status: HttpStatus.OK, type: [CourseTransactionDto] }) + @Get("/:providerId/course/transactions") + // Get transactions of all courses + async getCourseTransactions( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Res() res + ) { + try { + this.logger.log(`Getting course transactions`); + + const transactionsResponse = + await this.providerService.getCourseTransactions(providerId); + + this.logger.log(`Successfully retrieved course transactions`); + + res.status(HttpStatus.OK).json({ + message: "transactions fetched successfully", + data: transactionsResponse, + }); + } catch (err) { + this.logger.error(`Failed to fetch the transactions`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the transactions", + }); } - - @ApiOperation({ summary: 'Mark course as complete' }) - @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:providerId/course/completion") - // Mark course as complete for a user - async markCourseComplete( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Body() completeCourseDto: CompleteCourseDto, - @Res() res - ) { - try { - this.logger.log(`Updating course as complete`); - - await this.providerService.markCourseComplete(providerId, completeCourseDto); - - this.logger.log(`Successfully marked the course as complete`); - - res.status(HttpStatus.OK).json({ - message: "course marked complete", - }) - } catch (err) { - this.logger.error(`Failed to mark the course completion`); - - const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to mark the course completion", - }); - } + } + + @ApiOperation({ summary: "Mark course as complete" }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:providerId/course/completion") + // Mark course as complete for a user + async markCourseComplete( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Body() completeCourseDto: CompleteCourseDto, + @Res() res + ) { + try { + this.logger.log(`Updating course as complete`); + + await this.providerService.markCourseComplete( + providerId, + completeCourseDto + ); + + this.logger.log(`Successfully marked the course as complete`); + + res.status(HttpStatus.OK).json({ + message: "course marked complete", + }); + } catch (err) { + this.logger.error(`Failed to mark the course completion`); + + const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to mark the course completion", + }); + } + } + @ApiOperation({ summary: "Reset password" }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:providerId/reset-password") + // Reset Password + async resetPassword( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Body() updatePasswordDto: UpdatePasswordDto, + @Res() res + ) { + try { + this.logger.log("Reseting the password of the provider."); + await this.providerService.updateProviderPassword( + providerId, + updatePasswordDto + ); + this.logger.log(`Successfully reset the password.`); + + res.status(HttpStatus.OK).json({ + message: "Successfully reset the password.", + }); + } catch (error) { + this.logger.error(`Failed to reset the password.`); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to reset the password.", + }); } -} \ No newline at end of file + } +} diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index b175e3d..18e4df3 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -1,260 +1,312 @@ -import { BadRequestException, Injectable, NotAcceptableException, NotFoundException, UnauthorizedException } from '@nestjs/common'; -import { SignupDto } from './dto/signup.dto'; -import { PrismaService } from 'src/prisma/prisma.service'; -import { ProviderStatus } from '@prisma/client'; -import { LoginDto } from './dto/login.dto'; -import { UpdateProfileDto } from './dto/update-profile.dto'; -import { AddCourseDto } from 'src/course/dto/add-course.dto'; -import { CourseService } from 'src/course/course.service'; -import { Feedback, FeedbackResponseDto } from './dto/feedback.dto'; -import { CourseTransactionDto } from '../course/dto/transaction.dto'; -import { CompleteCourseDto } from 'src/course/dto/completion.dto'; -import { EditCourseDto } from 'src/course/dto/edit-course.dto'; -import { EditProvider } from 'src/admin/dto/edit-provider.dto'; -import { CourseResponse } from 'src/course/dto/course-response.dto'; -import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; -import { AuthService } from 'src/auth/auth.service'; -import axios from 'axios'; - +import { + BadRequestException, + Injectable, + NotAcceptableException, + NotFoundException, + UnauthorizedException, +} from "@nestjs/common"; +import { SignupDto } from "./dto/signup.dto"; +import { PrismaService } from "src/prisma/prisma.service"; +import { ProviderStatus } from "@prisma/client"; +import { LoginDto } from "./dto/login.dto"; +import { UpdateProfileDto } from "./dto/update-profile.dto"; +import { AddCourseDto } from "src/course/dto/add-course.dto"; +import { CourseService } from "src/course/course.service"; +import { Feedback, FeedbackResponseDto } from "./dto/feedback.dto"; +import { CourseTransactionDto } from "../course/dto/transaction.dto"; +import { CompleteCourseDto } from "src/course/dto/completion.dto"; +import { EditCourseDto } from "src/course/dto/edit-course.dto"; +import { EditProvider } from "src/admin/dto/edit-provider.dto"; +import { CourseResponse } from "src/course/dto/course-response.dto"; +import { ProviderProfileResponse } from "./dto/provider-profile-response.dto"; +import { AuthService } from "src/auth/auth.service"; +import axios from "axios"; +import { UpdatePasswordDto } from "./dto/update-password.dto"; @Injectable() export class ProviderService { - constructor( - private prisma: PrismaService, - private courseService: CourseService, - private authService: AuthService - ) {} - - async createNewAccount(signupDto: SignupDto) { - - // Check if email already exists - let provider = await this.prisma.provider.findUnique({ - where : { - email: signupDto.email - } - }); - - if(provider) - throw new BadRequestException("Account with that email ID already exists"); - - // Hashing the password - const hashedPassword = await this.authService.hashPassword(signupDto.password); - - // Create an entry in the database - provider = await this.prisma.provider.create({ - data: { - name: signupDto.name, - email: signupDto.email, - password: hashedPassword, - paymentInfo: signupDto.paymentInfo ? signupDto.paymentInfo : null - // other user profile data - } + constructor( + private prisma: PrismaService, + private courseService: CourseService, + private authService: AuthService + ) {} + + async createNewAccount(signupDto: SignupDto) { + // Check if email already exists + let provider = await this.prisma.provider.findUnique({ + where: { + email: signupDto.email, + }, + }); + + if (provider) + throw new BadRequestException( + "Account with that email ID already exists" + ); + + // Hashing the password + const hashedPassword = await this.authService.hashPassword( + signupDto.password + ); + + // Create an entry in the database + provider = await this.prisma.provider.create({ + data: { + name: signupDto.name, + email: signupDto.email, + password: hashedPassword, + paymentInfo: signupDto.paymentInfo ? signupDto.paymentInfo : null, + // other user profile data + }, + }); + + // Forward to wallet service for creation of wallet + const url = process.env.WALLET_SERVICE_URL; + const endpoint = url + `/api/wallet/create`; + const reqBody = { + userId: provider.id, + type: "PROVIDER", + }; + const resp = await axios.post(endpoint, reqBody); + + return provider.id; + } + + async getProviderIdFromLogin(loginDto: LoginDto) { + // Fetch the provider from email ID + const provider = await this.prisma.provider.findUnique({ + where: { + email: loginDto.email, + }, + }); + if (!provider) throw new NotFoundException("Email ID does not exist"); + + // Compare the entered password with the password fetched from database + const isPasswordValid = await this.authService.comparePasswords( + loginDto.password, + provider.password + ); + + if (!isPasswordValid) throw new BadRequestException("Incorrect password"); + + return provider.id; + } + + async getProvider(providerId: string) { + // Fetch provider details using ID + const provider = await this.prisma.provider.findUnique({ + where: { + id: providerId, + }, + }); + if (!provider) throw new NotFoundException("provider does not exist"); + + return provider; + } + + // Used when provider makes a request to update profile + async updateProfileInfo( + providerId: string, + updateProfileDto: UpdateProfileDto + ) { + await this.prisma.provider.update({ + where: { + id: providerId, + }, + data: updateProfileDto, + }); + } + + // Used when admin makes a request to update provider profile + async editProviderProfileByAdmin(profileInfo: EditProvider) { + return this.prisma.provider.update({ + where: { id: profileInfo.id }, + data: profileInfo, + }); + } + + async addNewCourse(providerId: string, addCourseDto: AddCourseDto) { + // Fetch provider + const provider = await this.getProvider(providerId); + + // Check verification + if (provider.status != ProviderStatus.VERIFIED) + throw new UnauthorizedException("Provider account is not verified"); + + // Forward to course service + return this.courseService.addCourse(providerId, addCourseDto); + } + + async removeCourse(providerId: string, courseId: number) { + // Validate course ID provided + const course = await this.courseService.getCourse(courseId); + if (!course) throw new NotFoundException("Course does not exist"); + + if (course.providerId != providerId) + throw new BadRequestException("Course does not belong to this provider"); + + // Forward to course service + await this.courseService.deleteCourse(courseId); + } + + async getCourses(providerId: string): Promise { + return this.courseService.getProviderCourses(providerId); + } + + async editCourse( + providerId: string, + courseId: number, + editCourseDto: EditCourseDto + ) { + // Validate provider + await this.getProvider(providerId); + + return this.courseService.editCourse(courseId, editCourseDto); + } + + async archiveCourse(providerId: string, courseId: number) { + // Validate provider + await this.getProvider(providerId); + + return this.courseService.archiveCourse(courseId); + } + + async getCourseFeedbacks( + providerId: string, + courseId: number + ): Promise { + // Fetch course + const course = await this.courseService.getCourse(courseId); + + // Validate course with provider + if (course.providerId != providerId) + throw new BadRequestException("Course does not belong to this provider"); + + // Forward to course service + const userCourses = await this.courseService.getPurchasedUsersByCourseId( + courseId + ); + + // Construction of DTO required for response + let feedbacks: Feedback[] = []; + for (let u of userCourses) { + if (u.feedback && u.rating) { + feedbacks.push({ + feedback: u.feedback, + rating: u.rating, }); - - // Forward to wallet service for creation of wallet - const url = process.env.WALLET_SERVICE_URL; - const endpoint = url + `/api/wallet/create`; - const reqBody = { - userId: provider.id, - type: 'PROVIDER' - } - const resp = await axios.post(endpoint, reqBody); - - return provider.id - } - - async getProviderIdFromLogin(loginDto: LoginDto) { - - // Fetch the provider from email ID - const provider = await this.prisma.provider.findUnique({ - where: { - email: loginDto.email - } - }) - if(!provider) - throw new NotFoundException("Email ID does not exist"); - - // Compare the entered password with the password fetched from database - const isPasswordValid = await this.authService.comparePasswords(loginDto.password, provider.password); - - if(!isPasswordValid) - throw new BadRequestException("Incorrect password"); - - return provider.id + } } - - async getProvider(providerId: string) { - - // Fetch provider details using ID - const provider = await this.prisma.provider.findUnique({ - where: { - id: providerId - } - }); - if(!provider) - throw new NotFoundException("provider does not exist"); - - return provider; - } - - // Used when provider makes a request to update profile - async updateProfileInfo(providerId: string, updateProfileDto: UpdateProfileDto) { - - await this.prisma.provider.update({ - where: { - id: providerId - }, - data: updateProfileDto - }) + return { + numberOfPurchases: userCourses.length, + feedbacks, + }; + } + + async getCourseTransactions( + providerId: string + ): Promise { + return this.courseService.getCourseTransactions(providerId); + } + + async markCourseComplete( + providerId: string, + completeCourseDto: CompleteCourseDto + ) { + // Validate course ID provided + const course = await this.courseService.getCourse( + completeCourseDto.courseId + ); + if (!course) throw new NotFoundException("Course does not exist"); + + if (course.providerId != providerId) + throw new BadRequestException("Course does not belong to this provider"); + + // Forward to course service. Error is thrown when user has not purchased a course + try { + await this.courseService.markCourseComplete(completeCourseDto); + } catch { + throw new NotFoundException( + "This user has not subscribed to this course" + ); } - - // Used when admin makes a request to update provider profile - async editProviderProfileByAdmin(profileInfo: EditProvider) { - - return this.prisma.provider.update({ - where: { id: profileInfo.id }, - data: profileInfo - }); - } - - async addNewCourse(providerId: string, addCourseDto: AddCourseDto) { - - // Fetch provider - const provider = await this.getProvider(providerId); - - // Check verification - if(provider.status != ProviderStatus.VERIFIED) - throw new UnauthorizedException("Provider account is not verified"); - - // Forward to course service - return this.courseService.addCourse(providerId, addCourseDto); - } - - async removeCourse(providerId: string, courseId: number) { - - // Validate course ID provided - const course = await this.courseService.getCourse(courseId); - if(!course) - throw new NotFoundException("Course does not exist"); - - if(course.providerId != providerId) - throw new BadRequestException("Course does not belong to this provider"); - - // Forward to course service - await this.courseService.deleteCourse(courseId); - } - - async getCourses(providerId: string): Promise { - - return this.courseService.getProviderCourses(providerId); + } + + async fetchAllProviders(): Promise { + return this.prisma.provider.findMany({ + select: { + id: true, + name: true, + email: true, + paymentInfo: true, + courses: true, + status: true, + }, + }); + } + + async verifyProvider(providerId: string) { + // Fetch provider + let providerInfo = await this.getProvider(providerId); + + // Check if provider verification is pending + if (providerInfo.status != ProviderStatus.PENDING) { + throw new NotAcceptableException( + `Provider is either verified or rejected.` + ); } - - async editCourse(providerId: string, courseId: number, editCourseDto: EditCourseDto) { - - // Validate provider - await this.getProvider(providerId); - - return this.courseService.editCourse(courseId, editCourseDto); - } - - async archiveCourse(providerId: string, courseId: number) { - - // Validate provider - await this.getProvider(providerId); - - return this.courseService.archiveCourse(courseId); - } - - async getCourseFeedbacks(providerId: string, courseId: number): Promise { - - // Fetch course - const course = await this.courseService.getCourse(courseId); - - // Validate course with provider - if(course.providerId != providerId) - throw new BadRequestException("Course does not belong to this provider"); - - // Forward to course service - const userCourses = await this.courseService.getPurchasedUsersByCourseId(courseId); - - // Construction of DTO required for response - let feedbacks: Feedback[] = []; - for(let u of userCourses) { - if(u.feedback && u.rating) { - feedbacks.push({ - feedback: u.feedback, - rating: u.rating - }) - } - } - return { - numberOfPurchases: userCourses.length, - feedbacks - }; - } - - async getCourseTransactions(providerId: string): Promise { - - return this.courseService.getCourseTransactions(providerId) - } - - async markCourseComplete(providerId: string, completeCourseDto: CompleteCourseDto) { - - // Validate course ID provided - const course = await this.courseService.getCourse(completeCourseDto.courseId); - if(!course) - throw new NotFoundException("Course does not exist"); - - if(course.providerId != providerId) - throw new BadRequestException("Course does not belong to this provider"); - - // Forward to course service. Error is thrown when user has not purchased a course - try { - await this.courseService.markCourseComplete(completeCourseDto); - } catch { - throw new NotFoundException("This user has not subscribed to this course"); - } - } - - async fetchAllProviders(): Promise { - - return this.prisma.provider.findMany({ - select: { id: true, name: true, email: true, paymentInfo: true, courses: true, status: true} - }); - } - - async verifyProvider(providerId: string) { - - // Fetch provider - let providerInfo = await this.getProvider(providerId); - - // Check if provider verification is pending - if(providerInfo.status != ProviderStatus.PENDING) { - throw new NotAcceptableException(`Provider is either verified or rejected.`); - } - // Update the status in database - return this.prisma.provider.update({ - where: {id: providerId}, - data: {status: ProviderStatus.VERIFIED} - }); - } - - async rejectProvider(providerId: string, rejectionReason: string) { - - // Fetch provider - let providerInfo = await this.getProvider(providerId); - - // Check if provider verification is pending - if(providerInfo.status != ProviderStatus.PENDING) { - throw new NotAcceptableException(`Provider is either already accepted or rejected`); - } - // Update the status in database - return this.prisma.provider.update({ - where: {id: providerId}, - data: { - status: ProviderStatus.REJECTED, - rejectionReason: rejectionReason - } - }); + // Update the status in database + return this.prisma.provider.update({ + where: { id: providerId }, + data: { status: ProviderStatus.VERIFIED }, + }); + } + + async rejectProvider(providerId: string, rejectionReason: string) { + // Fetch provider + let providerInfo = await this.getProvider(providerId); + + // Check if provider verification is pending + if (providerInfo.status != ProviderStatus.PENDING) { + throw new NotAcceptableException( + `Provider is either already accepted or rejected` + ); } -} \ No newline at end of file + // Update the status in database + return this.prisma.provider.update({ + where: { id: providerId }, + data: { + status: ProviderStatus.REJECTED, + rejectionReason: rejectionReason, + }, + }); + } + async updateProviderPassword( + providerId: string, + updatePasswordDto: UpdatePasswordDto + ) { + // validate the prrovider + const provider = await this.getProvider(providerId); + + // Compare the entered old password with the password fetched from database + const isPasswordValid = await this.authService.comparePasswords( + updatePasswordDto.oldPassword, + provider.password + ); + + if (!isPasswordValid) throw new BadRequestException("Incorrect password"); + // Hashing the password + const hashedPassword = await this.authService.hashPassword( + updatePasswordDto.newPassword + ); + // Updating the password to the newly generated one + return this.prisma.provider.update({ + where: { + id: providerId, + }, + data: { + password: hashedPassword, + }, + }); + } +} From ed3901c4ab75465a4c136de11bc47a686b6fdae3 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Wed, 29 Nov 2023 10:04:27 +0530 Subject: [PATCH 41/88] cqf score optional --- src/admin/admin.service.ts | 2 +- src/course/course.service.ts | 2 +- src/course/dto/verify-course.dto.ts | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index ea5f2c4..9135762 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -56,7 +56,7 @@ export class AdminService { } // accept a course along with the cqf score - async acceptCourse(courseId: number, cqf_score: number) { + async acceptCourse(courseId: number, cqf_score?: number) { return this.courseService.acceptCourse(courseId, cqf_score); } diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 6a1ac26..ed1664a 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -273,7 +273,7 @@ export class CourseService { return this.prisma.course.findMany(); } - async acceptCourse(courseId: number, cqf_score: number) { + async acceptCourse(courseId: number, cqf_score?: number) { // Validate course let course = await this.getCourse(courseId); diff --git a/src/course/dto/verify-course.dto.ts b/src/course/dto/verify-course.dto.ts index 7652b5d..a6bc10c 100644 --- a/src/course/dto/verify-course.dto.ts +++ b/src/course/dto/verify-course.dto.ts @@ -1,10 +1,10 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsNumber } from 'class-validator'; +import { IsNotEmpty, IsNumber, IsOptional } from 'class-validator'; export class CourseVerify { @ApiProperty() @IsNumber() - @IsNotEmpty() - cqf_score: number; + @IsOptional() + cqf_score?: number; } \ No newline at end of file From 98eb09f43f6e0395bc392c166fa414657ab6771f Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Wed, 29 Nov 2023 11:42:24 +0530 Subject: [PATCH 42/88] course creation date --- prisma/schema.prisma | 2 ++ prisma/seed.ts | 9 +++++---- src/course/course.service.ts | 15 ++++++++++++++- src/course/dto/course-response.dto.ts | 3 +++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5dea5ea..db3b9fb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -84,6 +84,8 @@ model Course { cqfScore Int? impactScore Float? rejectionReason String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt provider Provider @relation(fields: [providerId], references: [id]) userCourses UserCourse[] } diff --git a/prisma/seed.ts b/prisma/seed.ts index 9efedde..92277b6 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -28,7 +28,7 @@ async function main() { } }, - status: 'VERIFIED', + status: ProviderStatus.VERIFIED, orgLogo: "https://logos-world.net/wp-content/uploads/2021/11/Udemy-Logo.png", orgName: "Udemy", phone: "9999999999", @@ -47,7 +47,7 @@ async function main() { } }, - status: 'PENDING', + status: ProviderStatus.PENDING, orgLogo: "https://1000logos.net/wp-content/uploads/2022/06/Coursera-Logo-2012.png", orgName: "Coursera", phone: "9999999999", @@ -66,7 +66,7 @@ async function main() { } }, - status: 'REJECTED', + status: ProviderStatus.REJECTED, rejectionReason: "Invalid backAccNo", orgLogo: "https://upload.wikimedia.org/wikipedia/commons/thumb/6/6f/Logo_of_Twitter.svg/2491px-Logo_of_Twitter.svg.png", orgName: "Sunbird", @@ -92,7 +92,8 @@ async function main() { }, author: "Stephen Grider", startDate: new Date("2024-05-01").toISOString(), - endDate: new Date("2024-07-01").toISOString() + endDate: new Date("2024-07-01").toISOString(), + verificationStatus: CourseVerificationStatus.ACCEPTED, }, { providerId: provider1.id, title: "Graphic Design Masterclass", diff --git a/src/course/course.service.ts b/src/course/course.service.ts index ed1664a..006d562 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -149,10 +149,20 @@ export class CourseService { return course; } + async getNumOfCourseUsers(courseId: number) { + + return this.prisma.userCourse.count({ + where: { + courseId + } + }) + } + async getCourseByConsumer(courseId: number): Promise { // Find course by ID and throw error if not found const course = await this.getCourse(courseId); + const numOfUsers = await this.getNumOfCourseUsers(courseId); if(course.verificationStatus != CourseVerificationStatus.ACCEPTED) throw new BadRequestException("Course is not accepted"); @@ -162,7 +172,10 @@ export class CourseService { throw new BadRequestException("Course is not available at the moment"); const {cqfScore, impactScore, verificationStatus, rejectionReason, ...clone} = course; - return clone; + return { + ...clone, + numOfUsers + } } async giveCourseFeedback(courseId: number, userId: string, feedbackDto: FeedbackDto) { diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index 9d2b537..3ed5787 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -19,6 +19,9 @@ export class CourseResponse { readonly status: CourseStatus; readonly startDate: Date | null; readonly endDate: Date | null; + readonly createdAt: Date; + readonly updatedAt: Date; + readonly numOfUsers?: number; } export class ProviderCourseResponse extends CourseResponse { From e41d841037c6aa4a2f8ed544fb4a9ba821650a95 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico <144690071+SanchitEsMagico@users.noreply.github.com> Date: Wed, 29 Nov 2023 14:18:44 +0530 Subject: [PATCH 43/88] Revert "Added API for the reset password (#10)" (#11) This reverts commit c6f0a4ebc5fa20b87dd48f0ebbfb3453e9dd0559. --- src/provider/dto/update-password.dto.ts | 16 - src/provider/provider.controller.ts | 820 +++++++++++------------- src/provider/provider.service.ts | 556 ++++++++-------- 3 files changed, 632 insertions(+), 760 deletions(-) delete mode 100644 src/provider/dto/update-password.dto.ts diff --git a/src/provider/dto/update-password.dto.ts b/src/provider/dto/update-password.dto.ts deleted file mode 100644 index 754f357..0000000 --- a/src/provider/dto/update-password.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsStrongPassword } from "class-validator"; - -export class UpdatePasswordDto { - // old password - @ApiProperty() - @IsNotEmpty({ message: "Old Password is required" }) - @IsStrongPassword() - oldPassword: string; - - // new password - @ApiProperty() - @IsNotEmpty({ message: "New Password is required" }) - @IsStrongPassword() - newPassword: string; -} \ No newline at end of file diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index 9e60a70..63798a4 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -1,453 +1,393 @@ -import { - Body, - Controller, - Delete, - Get, - HttpStatus, - Logger, - Param, - ParseIntPipe, - ParseUUIDPipe, - Patch, - Post, - Put, - Res, -} from "@nestjs/common"; -import { ProviderService } from "./provider.service"; -import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; -import { SignupDto, SignupResponseDto } from "./dto/signup.dto"; -import { LoginDto, LoginResponseDto } from "./dto/login.dto"; -import { UpdateProfileDto } from "./dto/update-profile.dto"; -import { - AddCourseDto, - AddCourseResponseDto, -} from "src/course/dto/add-course.dto"; -import { FeedbackResponseDto } from "./dto/feedback.dto"; -import { CourseTransactionDto } from "../course/dto/transaction.dto"; -import { CompleteCourseDto } from "src/course/dto/completion.dto"; -import { EditCourseDto } from "src/course/dto/edit-course.dto"; -import { CourseResponse } from "src/course/dto/course-response.dto"; -import { ProviderProfileResponse } from "./dto/provider-profile-response.dto"; -import { getPrismaErrorStatusAndMessage } from "src/utils/utils"; -import { UpdatePasswordDto } from "./dto/update-password.dto"; - -@Controller("provider") -@ApiTags("provider") +import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Put, Res } from '@nestjs/common'; +import { ProviderService } from './provider.service'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { SignupDto, SignupResponseDto } from './dto/signup.dto'; +import { LoginDto, LoginResponseDto } from './dto/login.dto'; +import { UpdateProfileDto } from './dto/update-profile.dto'; +import { AddCourseDto, AddCourseResponseDto } from 'src/course/dto/add-course.dto'; +import { FeedbackResponseDto } from './dto/feedback.dto'; +import { CourseTransactionDto } from '../course/dto/transaction.dto'; +import { CompleteCourseDto } from 'src/course/dto/completion.dto'; +import { EditCourseDto } from 'src/course/dto/edit-course.dto'; +import { CourseResponse } from 'src/course/dto/course-response.dto'; +import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; +import { getPrismaErrorStatusAndMessage } from 'src/utils/utils'; + +@Controller('provider') +@ApiTags('provider') export class ProviderController { - private readonly logger = new Logger(ProviderController.name); - - constructor(private providerService: ProviderService) {} - - @ApiOperation({ summary: "create provider account" }) - @ApiResponse({ status: HttpStatus.CREATED, type: SignupResponseDto }) - @Post("/signup") - // create a new provider account - async createAccount(@Body() signupDto: SignupDto, @Res() res) { - try { - this.logger.log(`Creating new provider account`); - - const providerId = await this.providerService.createNewAccount(signupDto); - - this.logger.log(`successfully created new provider account`); - - res.status(HttpStatus.CREATED).json({ - message: "account created successfully", - data: { - providerId, - }, - }); - } catch (err) { - this.logger.error(`Failed to create new provider account`); - - const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to create new provider account`", - }); - } - } - - @ApiOperation({ summary: "provider login" }) - @ApiResponse({ status: HttpStatus.OK, type: LoginResponseDto }) - @Post("/login") - // provider login - async login(@Body() loginDto: LoginDto, @Res() res) { - try { - this.logger.log(`Getting provider ID`); - - const providerId = await this.providerService.getProviderIdFromLogin( - loginDto - ); - - this.logger.log(`successfully logged in`); - - res.status(HttpStatus.OK).json({ - message: "login successful", - data: { - providerId, - }, - }); - } catch (err) { - this.logger.error(`Failed to log in`); - - const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to log in`", - }); + + private readonly logger = new Logger(ProviderController.name); + + constructor( + private providerService: ProviderService, + ) {} + + @ApiOperation({ summary: 'create provider account' }) + @ApiResponse({ status: HttpStatus.CREATED, type: SignupResponseDto }) + @Post("/signup") + // create a new provider account + async createAccount( + @Body() signupDto: SignupDto, + @Res() res + ) { + try { + this.logger.log(`Creating new provider account`); + + const providerId = await this.providerService.createNewAccount(signupDto); + + this.logger.log(`successfully created new provider account`); + + res.status(HttpStatus.CREATED).json({ + message: "account created successfully", + data: { + providerId + } + }) + } catch (err) { + this.logger.error(`Failed to create new provider account`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to create new provider account`", + }); + } } - } - - @ApiOperation({ summary: "view provider profile" }) - @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse }) - @Get("/:providerId/profile") - // view provider profile information - async viewProfile( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Res() res - ) { - try { - this.logger.log(`Getting provider profile`); - - const provider = await this.providerService.getProvider(providerId); - - this.logger.log(`successfully retreived provider profile`); - - res.status(HttpStatus.OK).json({ - message: "fetch successful", - data: provider, - }); - } catch (err) { - this.logger.error(`Failed to retreive provider profile`); - - const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to retreive provider profile`", - }); + + @ApiOperation({ summary: 'provider login' }) + @ApiResponse({ status: HttpStatus.OK, type: LoginResponseDto }) + @Post("/login") + // provider login + async login( + @Body() loginDto: LoginDto, + @Res() res + ) { + try { + this.logger.log(`Getting provider ID`); + + const providerId = await this.providerService.getProviderIdFromLogin(loginDto); + + this.logger.log(`successfully logged in`); + + res.status(HttpStatus.OK).json({ + message: "login successful", + data: { + providerId + } + }) + } catch (err) { + this.logger.error(`Failed to log in`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to log in`", + }); + } } - } - - @ApiOperation({ summary: "update provider profile information" }) - @ApiResponse({ status: HttpStatus.OK }) - @Put("/:providerId/profile") - // update provider profile information - async updateProfile( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Body() updateProfileDto: UpdateProfileDto, - @Res() res - ) { - try { - this.logger.log(`Updating provider profile`); - - await this.providerService.updateProfileInfo( - providerId, - updateProfileDto - ); - - this.logger.log(`successfully updated provider profile`); - - res.status(HttpStatus.OK).json({ - message: "account updated successfully", - }); - } catch (err) { - this.logger.error(`Failed to update provider profile`); - - const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to update provider profile`", - }); + + @ApiOperation({ summary: 'view provider profile' }) + @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse }) + @Get("/:providerId/profile") + // view provider profile information + async viewProfile( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Res() res + ) { + try { + this.logger.log(`Getting provider profile`); + + const provider = await this.providerService.getProvider(providerId); + + this.logger.log(`successfully retreived provider profile`); + + res.status(HttpStatus.OK).json({ + message: "fetch successful", + data : provider + }) + } catch (err) { + this.logger.error(`Failed to retreive provider profile`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to retreive provider profile`", + }); + } } - } - - @ApiOperation({ summary: "edit course information" }) - @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:providerId/course/:courseId") - // edit course information - async editCourse( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, - @Body() editCourseDto: EditCourseDto, - @Res() res - ) { - try { - this.logger.log(`Updating course information`); - - await this.providerService.editCourse( - providerId, - courseId, - editCourseDto - ); - - this.logger.log(`Successfully updated course information`); - - res.status(HttpStatus.OK).json({ - message: "course edited successfully", - }); - } catch (err) { - this.logger.error(`Failed to update course information`); - - const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to update course information`", - }); + + @ApiOperation({ summary: 'update provider profile information' }) + @ApiResponse({ status: HttpStatus.OK }) + @Put("/:providerId/profile") + // update provider profile information + async updateProfile( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Body() updateProfileDto: UpdateProfileDto, + @Res() res + ) { + try { + this.logger.log(`Updating provider profile`); + + await this.providerService.updateProfileInfo(providerId, updateProfileDto); + + this.logger.log(`successfully updated provider profile`); + + res.status(HttpStatus.OK).json({ + message: "account updated successfully", + }) + } catch (err) { + this.logger.error(`Failed to update provider profile`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to update provider profile`", + }); + } } - } - - @ApiOperation({ summary: "Archive course" }) - @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:providerId/course/:courseId/archive") - // edit course information - async archiveCourse( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, - @Res() res - ) { - try { - this.logger.log(`Archiving course`); - - await this.providerService.archiveCourse(providerId, courseId); - - this.logger.log(`Successfully archived the course`); - - res.status(HttpStatus.OK).json({ - message: "course archived successfully", - }); - } catch (err) { - this.logger.error(`Failed to archive the course`); - - const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to archive the course`", - }); + + @ApiOperation({ summary: 'edit course information' }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:providerId/course/:courseId") + // edit course information + async editCourse( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Param("courseId", ParseIntPipe) courseId: number, + @Body() editCourseDto: EditCourseDto, + @Res() res + ) { + try { + this.logger.log(`Updating course information`); + + await this.providerService.editCourse(providerId, courseId, editCourseDto); + + this.logger.log(`Successfully updated course information`); + + res.status(HttpStatus.OK).json({ + message: "course edited successfully", + }) + } catch (err) { + this.logger.error(`Failed to update course information`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to update course information`", + }); + } } - } - - @ApiOperation({ summary: "add new course" }) - @ApiResponse({ status: HttpStatus.CREATED, type: AddCourseResponseDto }) - @Post("/:providerId/course") - // add new course - async addCourse( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Body() addCourseDto: AddCourseDto, - @Res() res - ) { - try { - this.logger.log(`Adding new course`); - - const course = await this.providerService.addNewCourse( - providerId, - addCourseDto - ); - - this.logger.log(`Successfully added new course`); - - res.status(HttpStatus.CREATED).json({ - message: "course added successfully", - data: course, - }); - } catch (err) { - this.logger.error(`Failed to add the course`); - - const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to add the course`", - }); + + @ApiOperation({ summary: 'Archive course' }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:providerId/course/:courseId/archive") + // edit course information + async archiveCourse( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Param("courseId", ParseIntPipe) courseId: number, + @Res() res + ) { + try { + this.logger.log(`Archiving course`); + + await this.providerService.archiveCourse(providerId, courseId); + + this.logger.log(`Successfully archived the course`); + + res.status(HttpStatus.OK).json({ + message: "course archived successfully", + }) + } catch (err) { + this.logger.error(`Failed to archive the course`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to archive the course`", + }); + } } - } - - @ApiOperation({ summary: "remove a course" }) - @ApiResponse({ status: HttpStatus.OK }) - @Delete("/:providerId/course/:courseId") - // remove an existing course - async removeCourse( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, - @Res() res - ) { - try { - this.logger.log(`Removing course`); - - await this.providerService.removeCourse(providerId, courseId); - - this.logger.log(`Successfully deleted the course`); - - res.status(HttpStatus.OK).json({ - message: "course deleted successfully", - }); - } catch (err) { - this.logger.error(`Failed to delete the course`); - - const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to delete the course`", - }); + + @ApiOperation({ summary: 'add new course' }) + @ApiResponse({ status: HttpStatus.CREATED, type: AddCourseResponseDto }) + @Post("/:providerId/course") + // add new course + async addCourse( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Body() addCourseDto: AddCourseDto, + @Res() res + ) { + try { + this.logger.log(`Adding new course`); + + const course = await this.providerService.addNewCourse(providerId, addCourseDto); + + this.logger.log(`Successfully added new course`); + + res.status(HttpStatus.CREATED).json({ + message: "course added successfully", + data: course + }) + } catch (err) { + this.logger.error(`Failed to add the course`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to add the course`", + }); + } } - } - - @ApiOperation({ summary: "View courses offered by self" }) - @ApiResponse({ status: HttpStatus.OK, type: [CourseResponse] }) - @Get("/:providerId/course") - // View courses offered by self - async fetchProviderCourses( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Res() res - ) { - try { - this.logger.log(`Getting courses`); - - const courses = await this.providerService.getCourses(providerId); - - this.logger.log(`Successfully retrieved the courses`); - - res.status(HttpStatus.OK).json({ - message: "courses fetched successfully", - data: courses, - }); - } catch (err) { - this.logger.error(`Failed to fetch the courses`); - - const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to fetch the courses", - }); + + @ApiOperation({ summary: 'remove a course' }) + @ApiResponse({ status: HttpStatus.OK }) + @Delete("/:providerId/course/:courseId") + // remove an existing course + async removeCourse( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Param("courseId", ParseIntPipe) courseId: number, + @Res() res + ) { + try { + this.logger.log(`Removing course`); + + await this.providerService.removeCourse(providerId, courseId); + + this.logger.log(`Successfully deleted the course`); + + + res.status(HttpStatus.OK).json({ + message: "course deleted successfully", + }) + } catch (err) { + this.logger.error(`Failed to delete the course`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to delete the course`", + }); + } } - } - - @ApiOperation({ - summary: "View Course Feedback & ratings, numberOfPurchases", - }) - @ApiResponse({ status: HttpStatus.OK, type: FeedbackResponseDto }) - @Get("/:providerId/course/:courseId/feedback") - // View Course Feedback & ratings, numberOfPurchases - async getCourseFeedback( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, - @Res() res - ) { - try { - this.logger.log(`Getting course feedbacks`); - - const feedbackResponse = await this.providerService.getCourseFeedbacks( - providerId, - courseId - ); - - this.logger.log(`Successfully retrieved the feedbacks`); - - res.status(HttpStatus.OK).json({ - message: "feedbacks fetched successfully", - data: feedbackResponse, - }); - } catch (err) { - this.logger.error(`Failed to fetch the feedbacks`); - - const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to fetch the feedbacks", - }); + + @ApiOperation({ summary: 'View courses offered by self' }) + @ApiResponse({ status: HttpStatus.OK, type: [CourseResponse] }) + @Get("/:providerId/course") + // View courses offered by self + async fetchProviderCourses( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Res() res + ) { + try { + this.logger.log(`Getting courses`); + + const courses = await this.providerService.getCourses(providerId); + + this.logger.log(`Successfully retrieved the courses`); + + res.status(HttpStatus.OK).json({ + message: "courses fetched successfully", + data: courses + }) + } catch (err) { + this.logger.error(`Failed to fetch the courses`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the courses", + }); + } } - } - - @ApiOperation({ summary: "Get transactions of all courses" }) - @ApiResponse({ status: HttpStatus.OK, type: [CourseTransactionDto] }) - @Get("/:providerId/course/transactions") - // Get transactions of all courses - async getCourseTransactions( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Res() res - ) { - try { - this.logger.log(`Getting course transactions`); - - const transactionsResponse = - await this.providerService.getCourseTransactions(providerId); - - this.logger.log(`Successfully retrieved course transactions`); - - res.status(HttpStatus.OK).json({ - message: "transactions fetched successfully", - data: transactionsResponse, - }); - } catch (err) { - this.logger.error(`Failed to fetch the transactions`); - - const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to fetch the transactions", - }); + + @ApiOperation({ summary: 'View Course Feedback & ratings, numberOfPurchases' }) + @ApiResponse({ status: HttpStatus.OK, type: FeedbackResponseDto }) + @Get("/:providerId/course/:courseId/feedback") + // View Course Feedback & ratings, numberOfPurchases + async getCourseFeedback( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Param("courseId", ParseIntPipe) courseId: number, + @Res() res + ) { + try { + this.logger.log(`Getting course feedbacks`); + + const feedbackResponse = await this.providerService.getCourseFeedbacks(providerId, courseId); + + this.logger.log(`Successfully retrieved the feedbacks`); + + res.status(HttpStatus.OK).json({ + message: "feedbacks fetched successfully", + data: feedbackResponse + }) + } catch (err) { + this.logger.error(`Failed to fetch the feedbacks`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the feedbacks", + }); + } } - } - - @ApiOperation({ summary: "Mark course as complete" }) - @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:providerId/course/completion") - // Mark course as complete for a user - async markCourseComplete( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Body() completeCourseDto: CompleteCourseDto, - @Res() res - ) { - try { - this.logger.log(`Updating course as complete`); - - await this.providerService.markCourseComplete( - providerId, - completeCourseDto - ); - - this.logger.log(`Successfully marked the course as complete`); - - res.status(HttpStatus.OK).json({ - message: "course marked complete", - }); - } catch (err) { - this.logger.error(`Failed to mark the course completion`); - - const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to mark the course completion", - }); + + @ApiOperation({ summary: 'Get transactions of all courses' }) + @ApiResponse({ status: HttpStatus.OK, type: [CourseTransactionDto] }) + @Get("/:providerId/course/transactions") + // Get transactions of all courses + async getCourseTransactions( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Res() res + ) { + try { + this.logger.log(`Getting course transactions`); + + const transactionsResponse = await this.providerService.getCourseTransactions(providerId); + + this.logger.log(`Successfully retrieved course transactions`); + + res.status(HttpStatus.OK).json({ + message: "transactions fetched successfully", + data: transactionsResponse + }) + } catch (err) { + this.logger.error(`Failed to fetch the transactions`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch the transactions", + }); + } } - } - @ApiOperation({ summary: "Reset password" }) - @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:providerId/reset-password") - // Reset Password - async resetPassword( - @Param("providerId", ParseUUIDPipe) providerId: string, - @Body() updatePasswordDto: UpdatePasswordDto, - @Res() res - ) { - try { - this.logger.log("Reseting the password of the provider."); - await this.providerService.updateProviderPassword( - providerId, - updatePasswordDto - ); - this.logger.log(`Successfully reset the password.`); - - res.status(HttpStatus.OK).json({ - message: "Successfully reset the password.", - }); - } catch (error) { - this.logger.error(`Failed to reset the password.`); - - const { errorMessage, statusCode } = - getPrismaErrorStatusAndMessage(error); - res.status(statusCode).json({ - statusCode, - message: errorMessage || "Failed to reset the password.", - }); + + @ApiOperation({ summary: 'Mark course as complete' }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:providerId/course/completion") + // Mark course as complete for a user + async markCourseComplete( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Body() completeCourseDto: CompleteCourseDto, + @Res() res + ) { + try { + this.logger.log(`Updating course as complete`); + + await this.providerService.markCourseComplete(providerId, completeCourseDto); + + this.logger.log(`Successfully marked the course as complete`); + + res.status(HttpStatus.OK).json({ + message: "course marked complete", + }) + } catch (err) { + this.logger.error(`Failed to mark the course completion`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to mark the course completion", + }); + } } - } -} +} \ No newline at end of file diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 18e4df3..b175e3d 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -1,312 +1,260 @@ -import { - BadRequestException, - Injectable, - NotAcceptableException, - NotFoundException, - UnauthorizedException, -} from "@nestjs/common"; -import { SignupDto } from "./dto/signup.dto"; -import { PrismaService } from "src/prisma/prisma.service"; -import { ProviderStatus } from "@prisma/client"; -import { LoginDto } from "./dto/login.dto"; -import { UpdateProfileDto } from "./dto/update-profile.dto"; -import { AddCourseDto } from "src/course/dto/add-course.dto"; -import { CourseService } from "src/course/course.service"; -import { Feedback, FeedbackResponseDto } from "./dto/feedback.dto"; -import { CourseTransactionDto } from "../course/dto/transaction.dto"; -import { CompleteCourseDto } from "src/course/dto/completion.dto"; -import { EditCourseDto } from "src/course/dto/edit-course.dto"; -import { EditProvider } from "src/admin/dto/edit-provider.dto"; -import { CourseResponse } from "src/course/dto/course-response.dto"; -import { ProviderProfileResponse } from "./dto/provider-profile-response.dto"; -import { AuthService } from "src/auth/auth.service"; -import axios from "axios"; -import { UpdatePasswordDto } from "./dto/update-password.dto"; +import { BadRequestException, Injectable, NotAcceptableException, NotFoundException, UnauthorizedException } from '@nestjs/common'; +import { SignupDto } from './dto/signup.dto'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { ProviderStatus } from '@prisma/client'; +import { LoginDto } from './dto/login.dto'; +import { UpdateProfileDto } from './dto/update-profile.dto'; +import { AddCourseDto } from 'src/course/dto/add-course.dto'; +import { CourseService } from 'src/course/course.service'; +import { Feedback, FeedbackResponseDto } from './dto/feedback.dto'; +import { CourseTransactionDto } from '../course/dto/transaction.dto'; +import { CompleteCourseDto } from 'src/course/dto/completion.dto'; +import { EditCourseDto } from 'src/course/dto/edit-course.dto'; +import { EditProvider } from 'src/admin/dto/edit-provider.dto'; +import { CourseResponse } from 'src/course/dto/course-response.dto'; +import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; +import { AuthService } from 'src/auth/auth.service'; +import axios from 'axios'; + @Injectable() export class ProviderService { - constructor( - private prisma: PrismaService, - private courseService: CourseService, - private authService: AuthService - ) {} - - async createNewAccount(signupDto: SignupDto) { - // Check if email already exists - let provider = await this.prisma.provider.findUnique({ - where: { - email: signupDto.email, - }, - }); - - if (provider) - throw new BadRequestException( - "Account with that email ID already exists" - ); - - // Hashing the password - const hashedPassword = await this.authService.hashPassword( - signupDto.password - ); - - // Create an entry in the database - provider = await this.prisma.provider.create({ - data: { - name: signupDto.name, - email: signupDto.email, - password: hashedPassword, - paymentInfo: signupDto.paymentInfo ? signupDto.paymentInfo : null, - // other user profile data - }, - }); - - // Forward to wallet service for creation of wallet - const url = process.env.WALLET_SERVICE_URL; - const endpoint = url + `/api/wallet/create`; - const reqBody = { - userId: provider.id, - type: "PROVIDER", - }; - const resp = await axios.post(endpoint, reqBody); - - return provider.id; - } - - async getProviderIdFromLogin(loginDto: LoginDto) { - // Fetch the provider from email ID - const provider = await this.prisma.provider.findUnique({ - where: { - email: loginDto.email, - }, - }); - if (!provider) throw new NotFoundException("Email ID does not exist"); - - // Compare the entered password with the password fetched from database - const isPasswordValid = await this.authService.comparePasswords( - loginDto.password, - provider.password - ); - - if (!isPasswordValid) throw new BadRequestException("Incorrect password"); - - return provider.id; - } - - async getProvider(providerId: string) { - // Fetch provider details using ID - const provider = await this.prisma.provider.findUnique({ - where: { - id: providerId, - }, - }); - if (!provider) throw new NotFoundException("provider does not exist"); - - return provider; - } - - // Used when provider makes a request to update profile - async updateProfileInfo( - providerId: string, - updateProfileDto: UpdateProfileDto - ) { - await this.prisma.provider.update({ - where: { - id: providerId, - }, - data: updateProfileDto, - }); - } - - // Used when admin makes a request to update provider profile - async editProviderProfileByAdmin(profileInfo: EditProvider) { - return this.prisma.provider.update({ - where: { id: profileInfo.id }, - data: profileInfo, - }); - } - - async addNewCourse(providerId: string, addCourseDto: AddCourseDto) { - // Fetch provider - const provider = await this.getProvider(providerId); - - // Check verification - if (provider.status != ProviderStatus.VERIFIED) - throw new UnauthorizedException("Provider account is not verified"); - - // Forward to course service - return this.courseService.addCourse(providerId, addCourseDto); - } - - async removeCourse(providerId: string, courseId: number) { - // Validate course ID provided - const course = await this.courseService.getCourse(courseId); - if (!course) throw new NotFoundException("Course does not exist"); - - if (course.providerId != providerId) - throw new BadRequestException("Course does not belong to this provider"); - - // Forward to course service - await this.courseService.deleteCourse(courseId); - } - - async getCourses(providerId: string): Promise { - return this.courseService.getProviderCourses(providerId); - } - - async editCourse( - providerId: string, - courseId: number, - editCourseDto: EditCourseDto - ) { - // Validate provider - await this.getProvider(providerId); - - return this.courseService.editCourse(courseId, editCourseDto); - } - - async archiveCourse(providerId: string, courseId: number) { - // Validate provider - await this.getProvider(providerId); - - return this.courseService.archiveCourse(courseId); - } - - async getCourseFeedbacks( - providerId: string, - courseId: number - ): Promise { - // Fetch course - const course = await this.courseService.getCourse(courseId); - - // Validate course with provider - if (course.providerId != providerId) - throw new BadRequestException("Course does not belong to this provider"); - - // Forward to course service - const userCourses = await this.courseService.getPurchasedUsersByCourseId( - courseId - ); - - // Construction of DTO required for response - let feedbacks: Feedback[] = []; - for (let u of userCourses) { - if (u.feedback && u.rating) { - feedbacks.push({ - feedback: u.feedback, - rating: u.rating, + constructor( + private prisma: PrismaService, + private courseService: CourseService, + private authService: AuthService + ) {} + + async createNewAccount(signupDto: SignupDto) { + + // Check if email already exists + let provider = await this.prisma.provider.findUnique({ + where : { + email: signupDto.email + } + }); + + if(provider) + throw new BadRequestException("Account with that email ID already exists"); + + // Hashing the password + const hashedPassword = await this.authService.hashPassword(signupDto.password); + + // Create an entry in the database + provider = await this.prisma.provider.create({ + data: { + name: signupDto.name, + email: signupDto.email, + password: hashedPassword, + paymentInfo: signupDto.paymentInfo ? signupDto.paymentInfo : null + // other user profile data + } }); - } + + // Forward to wallet service for creation of wallet + const url = process.env.WALLET_SERVICE_URL; + const endpoint = url + `/api/wallet/create`; + const reqBody = { + userId: provider.id, + type: 'PROVIDER' + } + const resp = await axios.post(endpoint, reqBody); + + return provider.id + } + + async getProviderIdFromLogin(loginDto: LoginDto) { + + // Fetch the provider from email ID + const provider = await this.prisma.provider.findUnique({ + where: { + email: loginDto.email + } + }) + if(!provider) + throw new NotFoundException("Email ID does not exist"); + + // Compare the entered password with the password fetched from database + const isPasswordValid = await this.authService.comparePasswords(loginDto.password, provider.password); + + if(!isPasswordValid) + throw new BadRequestException("Incorrect password"); + + return provider.id } - return { - numberOfPurchases: userCourses.length, - feedbacks, - }; - } - - async getCourseTransactions( - providerId: string - ): Promise { - return this.courseService.getCourseTransactions(providerId); - } - - async markCourseComplete( - providerId: string, - completeCourseDto: CompleteCourseDto - ) { - // Validate course ID provided - const course = await this.courseService.getCourse( - completeCourseDto.courseId - ); - if (!course) throw new NotFoundException("Course does not exist"); - - if (course.providerId != providerId) - throw new BadRequestException("Course does not belong to this provider"); - - // Forward to course service. Error is thrown when user has not purchased a course - try { - await this.courseService.markCourseComplete(completeCourseDto); - } catch { - throw new NotFoundException( - "This user has not subscribed to this course" - ); + + async getProvider(providerId: string) { + + // Fetch provider details using ID + const provider = await this.prisma.provider.findUnique({ + where: { + id: providerId + } + }); + if(!provider) + throw new NotFoundException("provider does not exist"); + + return provider; + } + + // Used when provider makes a request to update profile + async updateProfileInfo(providerId: string, updateProfileDto: UpdateProfileDto) { + + await this.prisma.provider.update({ + where: { + id: providerId + }, + data: updateProfileDto + }) } - } - - async fetchAllProviders(): Promise { - return this.prisma.provider.findMany({ - select: { - id: true, - name: true, - email: true, - paymentInfo: true, - courses: true, - status: true, - }, - }); - } - - async verifyProvider(providerId: string) { - // Fetch provider - let providerInfo = await this.getProvider(providerId); - - // Check if provider verification is pending - if (providerInfo.status != ProviderStatus.PENDING) { - throw new NotAcceptableException( - `Provider is either verified or rejected.` - ); + + // Used when admin makes a request to update provider profile + async editProviderProfileByAdmin(profileInfo: EditProvider) { + + return this.prisma.provider.update({ + where: { id: profileInfo.id }, + data: profileInfo + }); + } + + async addNewCourse(providerId: string, addCourseDto: AddCourseDto) { + + // Fetch provider + const provider = await this.getProvider(providerId); + + // Check verification + if(provider.status != ProviderStatus.VERIFIED) + throw new UnauthorizedException("Provider account is not verified"); + + // Forward to course service + return this.courseService.addCourse(providerId, addCourseDto); + } + + async removeCourse(providerId: string, courseId: number) { + + // Validate course ID provided + const course = await this.courseService.getCourse(courseId); + if(!course) + throw new NotFoundException("Course does not exist"); + + if(course.providerId != providerId) + throw new BadRequestException("Course does not belong to this provider"); + + // Forward to course service + await this.courseService.deleteCourse(courseId); + } + + async getCourses(providerId: string): Promise { + + return this.courseService.getProviderCourses(providerId); } - // Update the status in database - return this.prisma.provider.update({ - where: { id: providerId }, - data: { status: ProviderStatus.VERIFIED }, - }); - } - - async rejectProvider(providerId: string, rejectionReason: string) { - // Fetch provider - let providerInfo = await this.getProvider(providerId); - - // Check if provider verification is pending - if (providerInfo.status != ProviderStatus.PENDING) { - throw new NotAcceptableException( - `Provider is either already accepted or rejected` - ); + + async editCourse(providerId: string, courseId: number, editCourseDto: EditCourseDto) { + + // Validate provider + await this.getProvider(providerId); + + return this.courseService.editCourse(courseId, editCourseDto); + } + + async archiveCourse(providerId: string, courseId: number) { + + // Validate provider + await this.getProvider(providerId); + + return this.courseService.archiveCourse(courseId); + } + + async getCourseFeedbacks(providerId: string, courseId: number): Promise { + + // Fetch course + const course = await this.courseService.getCourse(courseId); + + // Validate course with provider + if(course.providerId != providerId) + throw new BadRequestException("Course does not belong to this provider"); + + // Forward to course service + const userCourses = await this.courseService.getPurchasedUsersByCourseId(courseId); + + // Construction of DTO required for response + let feedbacks: Feedback[] = []; + for(let u of userCourses) { + if(u.feedback && u.rating) { + feedbacks.push({ + feedback: u.feedback, + rating: u.rating + }) + } + } + return { + numberOfPurchases: userCourses.length, + feedbacks + }; + } + + async getCourseTransactions(providerId: string): Promise { + + return this.courseService.getCourseTransactions(providerId) + } + + async markCourseComplete(providerId: string, completeCourseDto: CompleteCourseDto) { + + // Validate course ID provided + const course = await this.courseService.getCourse(completeCourseDto.courseId); + if(!course) + throw new NotFoundException("Course does not exist"); + + if(course.providerId != providerId) + throw new BadRequestException("Course does not belong to this provider"); + + // Forward to course service. Error is thrown when user has not purchased a course + try { + await this.courseService.markCourseComplete(completeCourseDto); + } catch { + throw new NotFoundException("This user has not subscribed to this course"); + } + } + + async fetchAllProviders(): Promise { + + return this.prisma.provider.findMany({ + select: { id: true, name: true, email: true, paymentInfo: true, courses: true, status: true} + }); + } + + async verifyProvider(providerId: string) { + + // Fetch provider + let providerInfo = await this.getProvider(providerId); + + // Check if provider verification is pending + if(providerInfo.status != ProviderStatus.PENDING) { + throw new NotAcceptableException(`Provider is either verified or rejected.`); + } + // Update the status in database + return this.prisma.provider.update({ + where: {id: providerId}, + data: {status: ProviderStatus.VERIFIED} + }); + } + + async rejectProvider(providerId: string, rejectionReason: string) { + + // Fetch provider + let providerInfo = await this.getProvider(providerId); + + // Check if provider verification is pending + if(providerInfo.status != ProviderStatus.PENDING) { + throw new NotAcceptableException(`Provider is either already accepted or rejected`); + } + // Update the status in database + return this.prisma.provider.update({ + where: {id: providerId}, + data: { + status: ProviderStatus.REJECTED, + rejectionReason: rejectionReason + } + }); } - // Update the status in database - return this.prisma.provider.update({ - where: { id: providerId }, - data: { - status: ProviderStatus.REJECTED, - rejectionReason: rejectionReason, - }, - }); - } - async updateProviderPassword( - providerId: string, - updatePasswordDto: UpdatePasswordDto - ) { - // validate the prrovider - const provider = await this.getProvider(providerId); - - // Compare the entered old password with the password fetched from database - const isPasswordValid = await this.authService.comparePasswords( - updatePasswordDto.oldPassword, - provider.password - ); - - if (!isPasswordValid) throw new BadRequestException("Incorrect password"); - // Hashing the password - const hashedPassword = await this.authService.hashPassword( - updatePasswordDto.newPassword - ); - // Updating the password to the newly generated one - return this.prisma.provider.update({ - where: { - id: providerId, - }, - data: { - password: hashedPassword, - }, - }); - } -} +} \ No newline at end of file From 43f18e80081c3e3c88276e07c9a3a615521b4489 Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Wed, 29 Nov 2023 17:02:20 +0530 Subject: [PATCH 44/88] Modified search response type --- src/course/course.controller.ts | 6 +-- src/course/course.service.ts | 39 +++++++++++++++++-- ...r-course.dto.ts => search-response.dto.ts} | 10 +++-- 3 files changed, 45 insertions(+), 10 deletions(-) rename src/course/dto/{filter-course.dto.ts => search-response.dto.ts} (54%) diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index da485c5..25ab3d9 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -4,7 +4,7 @@ import { CourseService } from "./course.service"; import { FeedbackDto } from "./dto/feedback.dto"; import { CourseResponse } from "./dto/course-response.dto"; import { getPrismaErrorStatusAndMessage } from "src/utils/utils"; -import { FilterCourseDTO } from "./dto/filter-course.dto"; +import { SearchResponseDTO } from "./dto/search-response.dto"; @Controller('course') @ApiTags('course') @@ -140,13 +140,13 @@ export class CourseController { @Post("/verifyFilter") // Filter for admin verified courses async verifiedFilter( - @Body() courses: FilterCourseDTO[], + @Body() courses: SearchResponseDTO[], @Res() res ) { try { this.logger.log(`Filtering for courses verified by admin`); - const filteredCourses: FilterCourseDTO[] = await this.courseService.filterVerified(courses); + const filteredCourses: SearchResponseDTO[] = await this.courseService.filterVerified(courses); this.logger.log(`Successfully filtered the courses`); diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 69d8ddc..1c9eae6 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -7,7 +7,7 @@ import { CompleteCourseDto } from "./dto/completion.dto"; import { EditCourseDto } from "./dto/edit-course.dto"; import { AdminCourseResponse, CourseResponse } from "src/course/dto/course-response.dto"; import { CourseTransactionDto } from "./dto/transaction.dto"; -import { FilterCourseDTO } from "./dto/filter-course.dto"; +import { SearchResponseDTO } from "./dto/search-response.dto"; @Injectable() export class CourseService { @@ -15,7 +15,7 @@ export class CourseService { private prisma: PrismaService, ) {} - async searchCourses(searchInput: string): Promise { + async searchCourses(searchInput: string): Promise { // Searches for the courses available in the DB that match or contain the input search string // in their title, author, description or competency @@ -43,7 +43,38 @@ export class CourseService { }] } }); - return courses; + + const respPromises = courses.map(async (course) => { + const provider = await this.prisma.provider.findFirst({ + where: { + id: course.providerId + } + }); + const noOfPurchases = await this.prisma.userCourse.count({ + where: { + courseId: course.id + } + }); + const providerName = provider?.name || "null"; + return { + id: course.id.toString(), + title: course.title, + long_desc: course.description, + provider_id: course.providerId, + provider_name: providerName, + price: course.credits.toString(), + languages: course.language, + competency: course.competency, + imgUrl: course.imgLink, + rating: course.avgRating?.toString() || "0", + startTime: new Date().toISOString(), // need to change + endTime: new Date().toISOString(), // need to change + noOfPurchases: noOfPurchases.toString() + } + }); + + const resp = await Promise.all(respPromises); + return resp; } async addCourse(providerId: string, addCourseDto: AddCourseDto) { @@ -282,7 +313,7 @@ export class CourseService { }); } - async filterVerified(courses: FilterCourseDTO[]) { + async filterVerified(courses: SearchResponseDTO[]) { return courses.filter(async (course) => { const exists = await this.prisma.course.count({ diff --git a/src/course/dto/filter-course.dto.ts b/src/course/dto/search-response.dto.ts similarity index 54% rename from src/course/dto/filter-course.dto.ts rename to src/course/dto/search-response.dto.ts index 5f1a2a6..f68a911 100644 --- a/src/course/dto/filter-course.dto.ts +++ b/src/course/dto/search-response.dto.ts @@ -1,4 +1,6 @@ -export class FilterCourseDTO { +import { JsonValue } from "@prisma/client/runtime/library"; + +export class SearchResponseDTO { readonly id: string; readonly title: string; readonly long_desc: string; @@ -6,8 +8,10 @@ export class FilterCourseDTO { readonly provider_id: string; readonly price: string; readonly languages: string[]; + readonly competency: JsonValue; readonly imgUrl: string; readonly rating: string; - readonly duration: string; - readonly noOfPurchases: number; + readonly startTime: string; + readonly endTime: string; + readonly noOfPurchases: string; } \ No newline at end of file From fb7e17867ec4ad299748475a96f6293425512f4c Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Wed, 29 Nov 2023 17:10:35 +0530 Subject: [PATCH 45/88] provider name in search --- src/course/course.service.ts | 32 +++++++++++++++++++++++---- src/course/dto/course-response.dto.ts | 1 + 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 006d562..5d342ef 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -43,8 +43,16 @@ export class CourseService { string_contains: searchInput } }] + }, + include: { + provider: { + select: { + orgName: true, + } + } } }); + // Filter out the courses that are not accepted, archived or not available courses = courses.filter((c) => c.verificationStatus == CourseVerificationStatus.ACCEPTED && c.status == CourseStatus.UNARCHIVED @@ -52,8 +60,12 @@ export class CourseService { && (c.endDate ? c.endDate <= new Date(): true) ); return courses.map((c) => { - const {cqfScore, impactScore, verificationStatus, rejectionReason, ...clone} = c; - return clone; + let {cqfScore, impactScore, verificationStatus, rejectionReason, provider, ...clone} = c; + const courseResponse: CourseResponse = { + ...clone, + providerName: provider.orgName + } + return courseResponse; }); } @@ -141,12 +153,24 @@ export class CourseService { const course = await this.prisma.course.findUnique({ where: { id: courseId + }, + include: { + provider: { + select: { + orgName: true, + } + } } }) if(!course) throw new NotFoundException("Course does not exist"); - - return course; + + // let courseResponse: AdminCourseResponse + const { provider, ...courseResponse } = course; + return { + ...courseResponse, + providerName: provider.orgName + } } async getNumOfCourseUsers(courseId: number) { diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index 3ed5787..b360087 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -22,6 +22,7 @@ export class CourseResponse { readonly createdAt: Date; readonly updatedAt: Date; readonly numOfUsers?: number; + readonly providerName?: string; } export class ProviderCourseResponse extends CourseResponse { From 05e2363de1bbaa329b81f9d2c89a5fcb8964e3b8 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Wed, 29 Nov 2023 18:29:16 +0530 Subject: [PATCH 46/88] remove password in update profile --- src/admin/admin.controller.ts | 12 +--- src/admin/admin.service.ts | 3 +- src/admin/dto/edit-provider.dto.ts | 57 ++++++++++++++++++- .../dto/provider-profile-response.dto.ts | 10 +--- src/provider/provider.service.ts | 3 +- 5 files changed, 62 insertions(+), 23 deletions(-) diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index c0efadf..be9868b 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -139,16 +139,8 @@ export class AdminController { ){ try { this.logger.log(`Getting provider information for id ${providerId}`); - - const updatedProviderInfo = { - id: providerId, - name: providerDto.name, - email: providerDto.email, - password: providerDto.password, - status: providerDto.status - } - - const updatedProfile = await this.adminService.editProviderProfile(updatedProviderInfo); + + const updatedProfile = await this.adminService.editProviderProfile(providerDto); this.logger.log(`Successfully retrieved the provider profile information`); diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 9135762..ffc0b46 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -9,6 +9,7 @@ import { CourseService } from 'src/course/course.service'; import axios from 'axios'; import { AdminCourseResponse } from 'src/course/dto/course-response.dto'; import { ProviderSettlementDto } from './dto/provider-settlement.dto'; +import { ProviderProfileResponse } from 'src/provider/dto/provider-profile-response.dto'; @Injectable() export class AdminService { @@ -38,7 +39,7 @@ export class AdminService { } // fetch provider with the given id - async findProviderById(providerId: string): Promise { + async findProviderById(providerId: string): Promise { return this.providerService.getProvider(providerId); } diff --git a/src/admin/dto/edit-provider.dto.ts b/src/admin/dto/edit-provider.dto.ts index e90e466..e39375a 100644 --- a/src/admin/dto/edit-provider.dto.ts +++ b/src/admin/dto/edit-provider.dto.ts @@ -1,5 +1,56 @@ -import { PartialType } from "@nestjs/swagger"; -import { ProviderProfileResponse } from "../../provider/dto/provider-profile-response.dto"; +import { ApiProperty } from "@nestjs/swagger"; +import { IsEmail, IsEnum, IsObject, IsOptional, IsPhoneNumber, IsString, IsUUID, IsUrl } from "class-validator"; +import { ProviderStatus } from "@prisma/client"; +import { PaymentInfo } from "src/utils/types"; -export class EditProvider extends PartialType(ProviderProfileResponse){ + +export class EditProvider { + @ApiProperty({required: false}) + @IsUUID() + @IsOptional() + id?: string; + + @ApiProperty() + @IsOptional() + @IsString() + name?: string; + + @ApiProperty({format: 'email'}) + @IsEmail() + @IsOptional() + email?: string; + + // organisation name + @ApiProperty() + @IsOptional() + @IsString() + orgName?: string; + + // organisation logo image link + @ApiProperty() + @IsOptional() + @IsUrl() + orgLogo?: string; + + // phone number + @ApiProperty() + @IsOptional() + @IsPhoneNumber() + phone?: string; + + @ApiProperty() + @IsOptional() + @IsObject() + paymentInfo?: PaymentInfo; + + @ApiProperty() + @IsOptional() + @IsEnum(ProviderStatus) + status?: ProviderStatus; + + @ApiProperty() + @IsOptional() + @IsString() + rejectionReason?: string | null; + // readonly courses: Course[]; } \ No newline at end of file diff --git a/src/provider/dto/provider-profile-response.dto.ts b/src/provider/dto/provider-profile-response.dto.ts index c88ad5c..07f2bcd 100644 --- a/src/provider/dto/provider-profile-response.dto.ts +++ b/src/provider/dto/provider-profile-response.dto.ts @@ -1,8 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { ProviderStatus } from "@prisma/client"; +import { JsonValue } from "@prisma/client/runtime/library"; import { IsEmail, IsEnum, IsObject, IsOptional as IsNotEmpty, IsString, IsUUID, IsOptional, IsUrl, IsPhoneNumber } from 'class-validator'; -import { PaymentInfo } from "src/utils/types"; - export class ProviderProfileResponse { @ApiProperty({required: false}) @@ -19,11 +18,6 @@ export class ProviderProfileResponse { @IsEmail() @IsNotEmpty() email: string; - - @ApiProperty() - @IsString() - @IsNotEmpty() - password?: string; // organisation name @ApiProperty() @@ -46,7 +40,7 @@ export class ProviderProfileResponse { @ApiProperty() @IsOptional() @IsObject() - paymentInfo: PaymentInfo; + paymentInfo: JsonValue; @ApiProperty() @IsNotEmpty() diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 3e3bdaa..e0cd8d3 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -108,7 +108,8 @@ export class ProviderService { if(!provider) throw new NotFoundException("provider does not exist"); - return provider; + const { password, ...clone } = provider; + return clone; } async checkProviderFromEmail(email: string): Promise { From adaf86136bff88584c007db58092ce85e849eacc Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Thu, 30 Nov 2023 11:50:10 +0530 Subject: [PATCH 47/88] schema fixes --- prisma/schema.prisma | 3 ++- src/course/course.service.ts | 17 ++++++++++++++++- src/course/dto/course-response.dto.ts | 1 - .../dto/provider-profile-response.dto.ts | 12 +++++++++++- src/provider/provider.service.ts | 10 ++++++++-- 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index db3b9fb..9bcdfcb 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -60,6 +60,8 @@ model Provider { status ProviderStatus @default(PENDING) courses Course[] rejectionReason String? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) } @@ -73,7 +75,6 @@ model Course { credits Int noOfLessons Int? language String[] - duration Float competency Json author String avgRating Float? diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 5d342ef..4f6d43b 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -307,7 +307,22 @@ export class CourseService { async fetchAllCourses() : Promise { // Fetch all courses - return this.prisma.course.findMany(); + const courses = await this.prisma.course.findMany({ + include: { + provider: { + select: { + orgName: true, + } + } + } + }); + return courses.map((c) => { + const { provider, ...clone } = c; + return { + ...clone, + providerName: provider.orgName + } + }); } async acceptCourse(courseId: number, cqf_score?: number) { diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index b360087..d02a4e6 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -12,7 +12,6 @@ export class CourseResponse { readonly credits: number; readonly noOfLessons: number | null; readonly language: string[]; - readonly duration: number; readonly competency: JsonValue; readonly author: string; readonly avgRating: number | null; diff --git a/src/provider/dto/provider-profile-response.dto.ts b/src/provider/dto/provider-profile-response.dto.ts index 07f2bcd..bfee470 100644 --- a/src/provider/dto/provider-profile-response.dto.ts +++ b/src/provider/dto/provider-profile-response.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { ProviderStatus } from "@prisma/client"; import { JsonValue } from "@prisma/client/runtime/library"; -import { IsEmail, IsEnum, IsObject, IsOptional as IsNotEmpty, IsString, IsUUID, IsOptional, IsUrl, IsPhoneNumber } from 'class-validator'; +import { IsEmail, IsEnum, IsObject, IsOptional as IsNotEmpty, IsString, IsUUID, IsOptional, IsUrl, IsPhoneNumber, IsDate } from 'class-validator'; export class ProviderProfileResponse { @ApiProperty({required: false}) @@ -52,4 +52,14 @@ export class ProviderProfileResponse { @IsString() rejectionReason: string | null; // readonly courses: Course[]; + + @ApiProperty() + @IsNotEmpty() + @IsDate() + createdAt: Date; + + @ApiProperty() + @IsNotEmpty() + @IsDate() + updatedAt: Date; } \ No newline at end of file diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index e0cd8d3..38e349c 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -261,6 +261,8 @@ export class ProviderService { orgLogo: p.orgLogo, orgName: p.orgName, phone: p.phone, + createdAt: p.createdAt, + updatedAt: p.updatedAt } }) } @@ -315,7 +317,10 @@ export class ProviderService { // Update the status in database return this.prisma.provider.update({ where: {id: providerId}, - data: {status: ProviderStatus.VERIFIED} + data: { + status: ProviderStatus.VERIFIED, + updatedAt: new Date() + } }); } @@ -333,7 +338,8 @@ export class ProviderService { where: {id: providerId}, data: { status: ProviderStatus.REJECTED, - rejectionReason: rejectionReason + rejectionReason: rejectionReason, + updatedAt: new Date() } }); } From c5cfb9acc2e98dee90a5a13e83f24110746803d7 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Thu, 30 Nov 2023 15:32:09 +0530 Subject: [PATCH 48/88] purchase update --- src/course/course.service.ts | 7 +++++-- src/course/dto/purchase.dto.ts | 14 +------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 4f6d43b..651643b 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -82,6 +82,9 @@ export class CourseService { async addPurchaseRecord(courseId: number, purchaseDto: PurchaseDto) { + // Validate course + const course = await this.getCourse(courseId); + // Check if course already purchased const record = await this.prisma.userCourse.findFirst({ where: { userId: purchaseDto.consumerId, courseId: courseId } @@ -100,8 +103,8 @@ export class CourseService { try { const endpoint = `/api/consumers/${purchaseDto.consumerId}/purchase`; const walletPurchaseBody: WalletPurchaseDto = { - providerId: purchaseDto.providerId, - credits: purchaseDto.credits, + providerId: course.providerId, + credits: course.credits, description: purchaseDto.transactionDescription } const walletResponse = await axios.post(process.env.WALLET_SERVICE_URL + endpoint, walletPurchaseBody); diff --git a/src/course/dto/purchase.dto.ts b/src/course/dto/purchase.dto.ts index aafb5ef..41aa6c8 100644 --- a/src/course/dto/purchase.dto.ts +++ b/src/course/dto/purchase.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsIn, IsInt, IsNotEmpty, IsString, IsUUID } from "class-validator"; +import { IsInt, IsNotEmpty, IsString, IsUUID } from "class-validator"; export class PurchaseDto { @@ -9,18 +9,6 @@ export class PurchaseDto { @IsUUID() consumerId: string; - // provider ID - @ApiProperty() - @IsNotEmpty() - @IsUUID() - providerId: string; - - // Number of credits transferred - @ApiProperty() - @IsNotEmpty() - @IsInt() - credits: number; - // Purchase description @ApiProperty() @IsNotEmpty() From 0468f000253c113dc219ea4cac9cb0b2c11d5356 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 1 Dec 2023 11:47:10 +0530 Subject: [PATCH 49/88] seed update --- prisma/seed.ts | 7 ------- src/course/course.service.ts | 8 ++++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 92277b6..9058f9d 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -84,7 +84,6 @@ async function main() { credits: 4, noOfLessons: 3, language: ["en"], - duration: 4, competency: { "API Development": ["Level1", "Level2"], "Typescript": ["Level1"], @@ -103,7 +102,6 @@ async function main() { credits: 5, noOfLessons: 3, language: ["en"], - duration: 4, competency: { "Photoshop": ["Level2", "Level3"], "Understanding brand": ["Level1"] @@ -120,7 +118,6 @@ async function main() { credits: 2, noOfLessons: 3, language: ["en"], - duration: 4, competency: { "Statistics": ["Level1"], "Machine Learning": ["Level1", "Level2", "Level3"], @@ -136,7 +133,6 @@ async function main() { credits: 4, noOfLessons: 3, language: ["en"], - duration: 4, competency: { "Excel": ["Level1", "Level2", "Level3", "Level4"] }, @@ -184,7 +180,6 @@ async function main() { credits: 120, noOfLessons: 120, language: ["english", "hindi"], - duration: 48, competency: { "Docker": ["Level1", "Level3"], "Kubernetes": ["Level1"], @@ -210,7 +205,6 @@ async function main() { noOfLessons: 100, language: ["english", "hindi"], avgRating: 3.5, - duration: 50, competency: { "Logical Thinking": ["Level5", "Level4"], "Python": [ "Level1", "Level2" ] @@ -230,7 +224,6 @@ async function main() { credits: 160, noOfLessons: 100, language: ["english", "hindi"], - duration: 50, competency: { "Compiler Design": ["Level2", "Level3"], "LLVM": [ "Level4" ] diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 651643b..86b5cdb 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -84,6 +84,13 @@ export class CourseService { // Validate course const course = await this.getCourse(courseId); + + if(course.verificationStatus != CourseVerificationStatus.ACCEPTED) + throw new BadRequestException("Course is not accepted"); + if(course.status == CourseStatus.ARCHIVED) + throw new BadRequestException("Course is archived"); + if((course.startDate && course.startDate > new Date()) || (course.endDate && course.endDate < new Date())) + throw new BadRequestException("Course is not available at the moment"); // Check if course already purchased const record = await this.prisma.userCourse.findFirst({ @@ -92,6 +99,7 @@ export class CourseService { if(record != null) throw new BadRequestException("Course already purchased by the user"); + // create new record for purchase await this.prisma.userCourse.create({ data: { From d4c83487a9710c9a8634202511d307d0de1bb98e Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 1 Dec 2023 12:44:21 +0530 Subject: [PATCH 50/88] duration removed --- src/course/dto/add-course.dto.ts | 7 ------- src/course/dto/edit-course.dto.ts | 7 ------- 2 files changed, 14 deletions(-) diff --git a/src/course/dto/add-course.dto.ts b/src/course/dto/add-course.dto.ts index 9d3d36e..913a8bc 100644 --- a/src/course/dto/add-course.dto.ts +++ b/src/course/dto/add-course.dto.ts @@ -48,13 +48,6 @@ export class AddCourseDto { @ArrayNotEmpty() language: string[]; - // course duration - @ApiProperty() - @IsNotEmpty() - @Min(0) - @IsNumber() - duration: number; - // competency @ApiProperty() @IsNotEmpty() diff --git a/src/course/dto/edit-course.dto.ts b/src/course/dto/edit-course.dto.ts index 8b81a6b..43ccdee 100644 --- a/src/course/dto/edit-course.dto.ts +++ b/src/course/dto/edit-course.dto.ts @@ -47,13 +47,6 @@ export class EditCourseDto { @IsOptional() language?: string[]; - // course duration - @ApiProperty() - @Min(0) - @IsInt() - @IsOptional() - duration?: number; - // competency @ApiProperty() @IsOptional() From 78d111f67fdad94a329f6d58521904284fdb1920 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 1 Dec 2023 13:08:31 +0530 Subject: [PATCH 51/88] error handling --- src/course/course.service.ts | 2 +- src/provider/provider.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 86b5cdb..2ef83f6 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -127,7 +127,7 @@ export class CourseService { } } }); - throw new HttpException(err.response.data, err.response.status | err.status) + throw new HttpException(err.response.data || "Wallet service not running", err.response?.status || err.status || 500) } } diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 38e349c..72ea70f 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -72,7 +72,7 @@ export class ProviderService { id: provider.id } }); - throw new HttpException(err.response, err.response.status); + throw new HttpException(err.response || "Wallet service not running", err.response?.status || err.status || 500); } return provider.id } From 218b6a7384f1b960c583ad9446c51d7899c8ccff Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 1 Dec 2023 14:29:05 +0530 Subject: [PATCH 52/88] seed update --- prisma/schema.prisma | 1 - prisma/seed.ts | 95 +++++++++++++++++------------------- src/course/course.service.ts | 2 +- 3 files changed, 46 insertions(+), 52 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9bcdfcb..f96eaa1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -45,7 +45,6 @@ model Admin { name String email String @unique password String - walletId Int @unique } model Provider { diff --git a/prisma/seed.ts b/prisma/seed.ts index 9058f9d..6b6f895 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -11,7 +11,7 @@ async function main() { password: "9d209aacaed4088d68c41bd8dfb20de39cbd8339", status: ProviderStatus.VERIFIED, orgLogo: "https://logos-world.net/wp-content/uploads/2021/11/Udemy-Logo.png", - orgName: "Udemy", + orgName: "NPTEL", phone: "9999999999", } }); @@ -90,7 +90,7 @@ async function main() { "Backend engineering": ["Level1"] }, author: "Stephen Grider", - startDate: new Date("2024-05-01").toISOString(), + startDate: new Date("2023-05-01").toISOString(), endDate: new Date("2024-07-01").toISOString(), verificationStatus: CourseVerificationStatus.ACCEPTED, }, { @@ -107,7 +107,7 @@ async function main() { "Understanding brand": ["Level1"] }, author: "Lindsay Marsh", - startDate: new Date("2024-05-01").toISOString(), + startDate: new Date("2023-05-01").toISOString(), endDate: new Date("2024-09-01").toISOString() }, { providerId: provider1.id, @@ -124,6 +124,7 @@ async function main() { "MySQL": ["Level1"] }, author: "Jose Portilla", + verificationStatus: CourseVerificationStatus.ACCEPTED, }, { providerId: response.id, title: "Microsoft Excel", @@ -137,41 +138,8 @@ async function main() { "Excel": ["Level1", "Level2", "Level3", "Level4"] }, author: "Kyle Pew", - startDate: new Date("2024-05-01").toISOString() - }] - }) - - const admin1 = await prisma.admin.create({ - data: { - name: 'admin1', - email: "admin1@gmail.com", - password: "123456", - walletId: 2 - }, - }); - - const admin2 = await prisma.admin.create({ - data: { - name: 'admin2', - email: "admin2@gmail.com", - password: "admin", - walletId: 3 - }, - }); - - const admin3 = await prisma.admin.create({ - data: { - name: 'admin3', - email: "admin3@gmail.com", - password: "admin3", - walletId: 4 - }, - }); - - - - const course1 = await prisma.course.create({ - data: { + startDate: new Date("2024-05-01").toISOString(), + }, { providerId: provider1.id, title: "Learn DevOps & Kubernetes", description: "This course enables anyone to get started with devops engineering.", @@ -187,15 +155,11 @@ async function main() { }, author: "Jason Frig", startDate: new Date("2023-06-01"), - endDate: new Date("2023-08-01"), + endDate: new Date("2024-08-01"), avgRating: 3.9, verificationStatus: CourseVerificationStatus.ACCEPTED, cqfScore: 10, - } - }); - - const course2 = await prisma.course.create({ - data: { + }, { providerId: provider1.id, title: "Introduction to Programming", description: "This course covers all the fundamentals of programming", @@ -211,11 +175,7 @@ async function main() { }, author: "James Franco", verificationStatus: CourseVerificationStatus.PENDING, - } - }); - - const course3 = await prisma.course.create({ - data: { + }, { providerId: provider1.id, title: "Introduction to Compiler Engineering", description: "This course covers how compilers are built and also teaches you about how to create custom programming languages", @@ -233,9 +193,44 @@ async function main() { endDate: new Date("2023-11-10"), verificationStatus: CourseVerificationStatus.REJECTED, rejectionReason: "Level associated with LLVM is wrong" + }, { + providerId: provider1.id, + title: "Introduction to Compiler Engineering 2", + description: "This course covers how compilers are built and also teaches you about how to create custom programming languages", + courseLink: "https://udemy.com/courses/jQKsLpm", + imgLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", + credits: 160, + noOfLessons: 100, + language: ["english", "hindi"], + competency: { + "Compiler Design": ["Level2", "Level3"], + "LLVM": [ "Level4" ] + }, + author: "Ramakrishna Upadrasta", + startDate: new Date("2023-10-10"), + endDate: new Date("2023-11-10"), + rejectionReason: "Level associated with LLVM is wrong", + status: CourseStatus.ARCHIVED + }] + }) + + const admin = await prisma.admin.create({ + data: { + name: "Sanchit Uke", + email: "sanchit@esmagico.in", + password: "asdfghjkl", + id: "123e4567-e89b-42d3-a456-556642440020", } }); + const admin1 = await prisma.admin.create({ + data: { + name: 'admin1', + email: "admin1@gmail.com", + password: "123456", + }, + }); + const resp = await prisma.course.findMany({}); console.log("All courses: ", resp); const response3 = await prisma.userCourse.createMany({ @@ -262,7 +257,7 @@ async function main() { }] }) console.log(response) - console.log({ response1, response3, admin1, admin2, admin3, provider1, provider2, provider3, course1, course2, course3 }); + console.log({ response1, response3, admin1, provider1, provider2, provider3, admin }); } diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 2ef83f6..ec53e44 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -57,7 +57,7 @@ export class CourseService { c.verificationStatus == CourseVerificationStatus.ACCEPTED && c.status == CourseStatus.UNARCHIVED && (c.startDate ? c.startDate <= new Date(): true) - && (c.endDate ? c.endDate <= new Date(): true) + && (c.endDate ? c.endDate >= new Date(): true) ); return courses.map((c) => { let {cqfScore, impactScore, verificationStatus, rejectionReason, provider, ...clone} = c; From 66ee8a07e30e018efba15a87b021d56fc0e3ba5d Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 1 Dec 2023 15:03:37 +0530 Subject: [PATCH 53/88] bug fix --- prisma/seed.ts | 4 ++-- src/course/course.service.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 6b6f895..f6fe5a3 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -108,7 +108,8 @@ async function main() { }, author: "Lindsay Marsh", startDate: new Date("2023-05-01").toISOString(), - endDate: new Date("2024-09-01").toISOString() + endDate: new Date("2024-09-01").toISOString(), + verificationStatus: CourseVerificationStatus.ACCEPTED, }, { providerId: provider1.id, title: "Python for Data Science", @@ -124,7 +125,6 @@ async function main() { "MySQL": ["Level1"] }, author: "Jose Portilla", - verificationStatus: CourseVerificationStatus.ACCEPTED, }, { providerId: response.id, title: "Microsoft Excel", diff --git a/src/course/course.service.ts b/src/course/course.service.ts index ec53e44..15b0421 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -127,7 +127,7 @@ export class CourseService { } } }); - throw new HttpException(err.response.data || "Wallet service not running", err.response?.status || err.status || 500) + throw new HttpException(err.response || "Wallet service not running", err.response?.status || err.status || 500) } } From 11d8ee0592ba46dd2d6baa6c2d6c1a8bb2e35977 Mon Sep 17 00:00:00 2001 From: Prashant Kumar <144701828+prashantesmagico@users.noreply.github.com> Date: Fri, 1 Dec 2023 15:29:15 +0530 Subject: [PATCH 54/88] Added API for reset password (#12) Co-authored-by: SanchitEsMagico <144690071+SanchitEsMagico@users.noreply.github.com> --- package-lock.json | 40 ++++++++++++------------- src/provider/dto/update-password.dto.ts | 16 ++++++++++ src/provider/provider.controller.ts | 33 ++++++++++++++++++++ src/provider/provider.service.ts | 30 +++++++++++++++++++ 4 files changed, 99 insertions(+), 20 deletions(-) create mode 100644 src/provider/dto/update-password.dto.ts diff --git a/package-lock.json b/package-lock.json index b0962d6..d13b609 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1445,9 +1445,9 @@ } }, "node_modules/@nestjs/axios": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.0.tgz", - "integrity": "sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.1.tgz", + "integrity": "sha512-VlOZhAGDmOoFdsmewn8AyClAdGpKXQQaY1+3PGB+g6ceurGIdTxZgRX3VXc1T6Zs60PedWjg3A82TDOB05mrzQ==", "peerDependencies": { "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", "axios": "^1.3.1", @@ -1706,9 +1706,9 @@ } }, "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==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.3.tgz", + "integrity": "sha512-40Zdqg98lqoF0+7ThWIZFStxgzisK6GG22+1ABO4kZiGF/Tu2FE+DYLw+Q9D94vcFWizJ+MSjNN4ns9r6hIGxw==", "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "class-transformer": "^0.4.0 || ^0.5.0", @@ -1760,15 +1760,15 @@ } }, "node_modules/@nestjs/swagger": { - "version": "7.1.14", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.14.tgz", - "integrity": "sha512-2Ol4S6qHeYVVmkshkWBM8E/qkmEqEOUj2QIewr0jLSyo30H7f3v81pJyks6pTLy4PK0LGUXojMvIfFIE3mmGQQ==", + "version": "7.1.16", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.16.tgz", + "integrity": "sha512-f9KBk/BX9MUKPTj7tQNYJ124wV/jP5W2lwWHLGwe/4qQXixuDOo39zP55HIJ44LE7S04B7BOeUOo9GBJD/vRcw==", "dependencies": { - "@nestjs/mapped-types": "2.0.2", + "@nestjs/mapped-types": "2.0.3", "js-yaml": "4.1.0", "lodash": "4.17.21", "path-to-regexp": "3.2.0", - "swagger-ui-dist": "5.9.0" + "swagger-ui-dist": "5.9.1" }, "peerDependencies": { "@fastify/static": "^6.0.0", @@ -2239,9 +2239,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.200", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz", - "integrity": "sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==", + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", "dev": true }, "node_modules/@types/mime": { @@ -2939,9 +2939,9 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.1.tgz", - "integrity": "sha512-vfBmhDpKafglh0EldBEbVuoe7DyAavGSLWhuSm5ZSEKQnHhBf0xAAwybbNH1IkrJNGnS/VG4I5yxig1pCEXE4g==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", + "integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==", "dependencies": { "follow-redirects": "^1.15.0", "form-data": "^4.0.0", @@ -7843,9 +7843,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.9.0.tgz", - "integrity": "sha512-NUHSYoe5XRTk/Are8jPJ6phzBh3l9l33nEyXosM17QInoV95/jng8+PuSGtbD407QoPf93MH3Bkh773OgesJpA==" + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.9.1.tgz", + "integrity": "sha512-5zAx+hUwJb9T3EAntc7TqYkV716CMqG6sZpNlAAMOMWkNXRYxGkN8ADIvD55dQZ10LxN90ZM/TQmN7y1gpICnw==" }, "node_modules/swagger-ui-express": { "version": "5.0.0", diff --git a/src/provider/dto/update-password.dto.ts b/src/provider/dto/update-password.dto.ts new file mode 100644 index 0000000..754f357 --- /dev/null +++ b/src/provider/dto/update-password.dto.ts @@ -0,0 +1,16 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsNotEmpty, IsStrongPassword } from "class-validator"; + +export class UpdatePasswordDto { + // old password + @ApiProperty() + @IsNotEmpty({ message: "Old Password is required" }) + @IsStrongPassword() + oldPassword: string; + + // new password + @ApiProperty() + @IsNotEmpty({ message: "New Password is required" }) + @IsStrongPassword() + newPassword: string; +} \ No newline at end of file diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index 15cedcf..4d18891 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -12,6 +12,7 @@ import { EditCourseDto } from 'src/course/dto/edit-course.dto'; import { ProviderCourseResponse } from 'src/course/dto/course-response.dto'; import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; import { getPrismaErrorStatusAndMessage } from 'src/utils/utils'; +import { UpdatePasswordDto } from './dto/update-password.dto'; import { CourseStatusDto } from 'src/course/dto/course-status.dto'; @Controller('provider') @@ -421,4 +422,36 @@ export class ProviderController { }); } } + + @ApiOperation({ summary: "Reset password" }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:providerId/reset-password") + // Reset Password + async resetPassword( + @Param("providerId", ParseUUIDPipe) providerId: string, + @Body() updatePasswordDto: UpdatePasswordDto, + @Res() res + ) { + try { + this.logger.log("Reseting the password of the provider."); + await this.providerService.updateProviderPassword( + providerId, + updatePasswordDto + ); + this.logger.log(`Successfully reset the password.`); + + res.status(HttpStatus.OK).json({ + message: "Successfully reset the password.", + }); + } catch (error) { + this.logger.error(`Failed to reset the password.`); + + const { errorMessage, statusCode } = + getPrismaErrorStatusAndMessage(error); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to reset the password.", + }); + } + } } \ No newline at end of file diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 72ea70f..284808e 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -15,6 +15,7 @@ import { ProviderCourseResponse } from 'src/course/dto/course-response.dto'; import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; import { AuthService } from 'src/auth/auth.service'; import axios from 'axios'; +import { UpdatePasswordDto } from './dto/update-password.dto'; import { CourseStatusDto } from 'src/course/dto/course-status.dto'; import { ProviderSettlementDto } from 'src/admin/dto/provider-settlement.dto'; @@ -343,4 +344,33 @@ export class ProviderService { } }); } + + async updateProviderPassword( + providerId: string, + updatePasswordDto: UpdatePasswordDto + ) { + // validate the prrovider + const provider = await this.getProvider(providerId); + + // Compare the entered old password with the password fetched from database + const isPasswordValid = await this.authService.comparePasswords( + updatePasswordDto.oldPassword, + provider.password + ); + + if (!isPasswordValid) throw new BadRequestException("Incorrect password"); + // Hashing the password + const hashedPassword = await this.authService.hashPassword( + updatePasswordDto.newPassword + ); + // Updating the password to the newly generated one + return this.prisma.provider.update({ + where: { + id: providerId, + }, + data: { + password: hashedPassword, + }, + }); + } } \ No newline at end of file From 67a9559dacb4dd49fa9e9d184278ebd6e985f9ec Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 1 Dec 2023 15:41:41 +0530 Subject: [PATCH 55/88] merge fixes --- prisma/seed.ts | 7 +++++-- src/provider/dto/update-password.dto.ts | 1 - src/provider/provider.service.ts | 10 ++++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index f6fe5a3..c1d4821 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,14 +1,17 @@ import { CourseStatus, CourseVerificationStatus, PrismaClient, ProviderStatus } from '@prisma/client' +import * as bcrypt from 'bcrypt'; const prisma = new PrismaClient() async function main() { - + + const saltRounds = 10; + const hashedPassword = await bcrypt.hash("123456", saltRounds); const response = await prisma.provider.create({ data: { id: "123e4567-e89b-42d3-a456-556642440010", name: "Vijay Salgaonkar", email: "vijaysalgaonkar@gmail.com", - password: "9d209aacaed4088d68c41bd8dfb20de39cbd8339", + password: hashedPassword, status: ProviderStatus.VERIFIED, orgLogo: "https://logos-world.net/wp-content/uploads/2021/11/Udemy-Logo.png", orgName: "NPTEL", diff --git a/src/provider/dto/update-password.dto.ts b/src/provider/dto/update-password.dto.ts index 754f357..0ea9f19 100644 --- a/src/provider/dto/update-password.dto.ts +++ b/src/provider/dto/update-password.dto.ts @@ -5,7 +5,6 @@ export class UpdatePasswordDto { // old password @ApiProperty() @IsNotEmpty({ message: "Old Password is required" }) - @IsStrongPassword() oldPassword: string; // new password diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 284808e..986a686 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -349,8 +349,14 @@ export class ProviderService { providerId: string, updatePasswordDto: UpdatePasswordDto ) { - // validate the prrovider - const provider = await this.getProvider(providerId); + // Fetch provider details using ID + const provider = await this.prisma.provider.findUnique({ + where: { + id: providerId + } + }); + if(!provider) + throw new NotFoundException("provider does not exist"); // Compare the entered old password with the password fetched from database const isPasswordValid = await this.authService.comparePasswords( From 5f04475abc280761c88c6ae49756788dda690b80 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Tue, 5 Dec 2023 17:12:55 +0530 Subject: [PATCH 56/88] courseId uuid --- package-lock.json | 430 +++++++++++++++++++++++++- package.json | 1 + prisma/schema.prisma | 4 +- prisma/seed.ts | 34 +- src/admin/admin.controller.ts | 8 +- src/admin/admin.service.ts | 8 +- src/course/course.controller.ts | 6 +- src/course/course.service.ts | 26 +- src/course/dto/completion.dto.ts | 4 +- src/course/dto/course-response.dto.ts | 2 +- src/course/dto/purchase.dto.ts | 8 +- src/course/dto/transaction.dto.ts | 2 +- src/provider/provider.controller.ts | 15 +- src/provider/provider.service.ts | 12 +- src/utils/minio.ts | 34 ++ 15 files changed, 522 insertions(+), 72 deletions(-) create mode 100644 src/utils/minio.ts diff --git a/package-lock.json b/package-lock.json index d13b609..c4465ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "lodash": "^4.17.21", + "minio": "^7.1.3", "nestjs-prisma": "^0.22.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", @@ -2698,6 +2699,12 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/@zxing/text-encoding": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", + "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", + "optional": true + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -2933,11 +2940,27 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/axios": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz", @@ -3108,6 +3131,27 @@ "node": ">= 6" } }, + "node_modules/block-stream2": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", + "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", + "dependencies": { + "readable-stream": "^3.4.0" + } + }, + "node_modules/block-stream2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -3165,6 +3209,11 @@ "node": ">=8" } }, + "node_modules/browser-or-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", + "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==" + }, "node_modules/browserslist": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", @@ -3241,6 +3290,14 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3266,12 +3323,13 @@ } }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3701,6 +3759,14 @@ } } }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -3733,6 +3799,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4429,6 +4508,27 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "node_modules/fast-xml-parser": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.3.2.tgz", + "integrity": "sha512-rmrXUXwbJedoXkStenj1kkljNF7ugn5ZjR9FJcwmCfcCbtOMDghPajbc+Tck6vE6F5XsDmx+Pr2le9fw8+pXBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -4495,6 +4595,14 @@ "node": ">=8" } }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -4580,6 +4688,14 @@ } } }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, "node_modules/fork-ts-checker-webpack-plugin": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-8.0.0.tgz", @@ -4719,9 +4835,12 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/gauge": { "version": "3.0.2", @@ -4761,14 +4880,14 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4867,6 +4986,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4883,6 +5013,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz", "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==", + "dev": true, "engines": { "node": ">= 0.4.0" } @@ -4895,6 +5026,17 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", @@ -4917,11 +5059,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", @@ -5113,6 +5280,21 @@ "node": ">= 0.10" } }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -5131,6 +5313,17 @@ "node": ">=8" } }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", @@ -5169,6 +5362,20 @@ "node": ">=6" } }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5219,6 +5426,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", + "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "dependencies": { + "which-typed-array": "^1.1.11" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -5959,6 +6180,11 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-stream/-/json-stream-1.0.0.tgz", + "integrity": "sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -6277,6 +6503,38 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minio": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minio/-/minio-7.1.3.tgz", + "integrity": "sha512-xPrLjWkTT5E7H7VnzOjF//xBp9I40jYB4aWhb2xTFopXXfw+Wo82DDWngdUju7Doy3Wk7R8C4LAgwhLHHnf0wA==", + "dependencies": { + "async": "^3.2.4", + "block-stream2": "^2.1.0", + "browser-or-node": "^2.1.1", + "buffer-crc32": "^0.2.13", + "fast-xml-parser": "^4.2.2", + "ipaddr.js": "^2.0.1", + "json-stream": "^1.0.0", + "lodash": "^4.17.21", + "mime-types": "^2.1.35", + "query-string": "^7.1.3", + "through2": "^4.0.2", + "web-encoding": "^1.1.5", + "xml": "^1.0.1", + "xml2js": "^0.5.0" + }, + "engines": { + "node": "^16 || ^18 || >=20" + } + }, + "node_modules/minio/node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "engines": { + "node": ">= 10" + } + }, "node_modules/minipass": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", @@ -7101,6 +7359,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -7379,6 +7654,11 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -7527,6 +7807,20 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -7637,6 +7931,14 @@ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", "deprecated": "Please use @jridgewell/sourcemap-codec instead" }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "engines": { + "node": ">=6" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -7680,6 +7982,14 @@ "node": ">=10.0.0" } }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -7760,6 +8070,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/superagent": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz", @@ -8048,6 +8363,27 @@ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", "dev": true }, + "node_modules/through2": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", + "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", + "dependencies": { + "readable-stream": "3" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -8429,6 +8765,18 @@ "punycode": "^2.1.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -8522,6 +8870,17 @@ "defaults": "^1.0.3" } }, + "node_modules/web-encoding": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", + "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", + "dependencies": { + "util": "^0.12.3" + }, + "optionalDependencies": { + "@zxing/text-encoding": "0.9.0" + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -8617,6 +8976,24 @@ "node": ">= 8" } }, + "node_modules/which-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", + "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.4", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wide-align": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", @@ -8722,6 +9099,31 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 53a6ee0..3cf534b 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "lodash": "^4.17.21", + "minio": "^7.1.3", "nestjs-prisma": "^0.22.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f96eaa1..0c409e4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -65,7 +65,7 @@ model Provider { model Course { - id Int @id @default(autoincrement()) + id String @id @default(uuid()) providerId String title String description String @@ -93,7 +93,7 @@ model Course { model UserCourse { id Int @id @default(autoincrement()) userId String @db.Uuid - courseId Int + courseId String purchasedAt DateTime @default(now()) status CourseProgressStatus @default(IN_PROGRESS) courseCompletionScore Float? diff --git a/prisma/seed.ts b/prisma/seed.ts index c1d4821..690f47a 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -79,6 +79,7 @@ async function main() { const response1 = await prisma.course.createMany({ data: [{ + id: "123e4567-e89b-42d3-a456-556642440050", providerId: provider1.id, title: "NestJS Complete", description: "Build full featured backend APIs incredibly quickly with Nest, TypeORM, and Typescript. Includes testing and deployment!", @@ -97,6 +98,7 @@ async function main() { endDate: new Date("2024-07-01").toISOString(), verificationStatus: CourseVerificationStatus.ACCEPTED, }, { + id: "123e4567-e89b-42d3-a456-556642440051", providerId: provider1.id, title: "Graphic Design Masterclass", description: "The Ultimate Graphic Design Course Which Covers Photoshop, Illustrator, InDesign, Design Theory, Branding & Logo Design", @@ -114,6 +116,7 @@ async function main() { endDate: new Date("2024-09-01").toISOString(), verificationStatus: CourseVerificationStatus.ACCEPTED, }, { + id: "123e4567-e89b-42d3-a456-556642440052", providerId: provider1.id, title: "Python for Data Science", description: "Learn how to use NumPy, Pandas, Seaborn , Matplotlib , Plotly , Scikit-Learn , Machine Learning, Tensorflow , and more", @@ -129,6 +132,7 @@ async function main() { }, author: "Jose Portilla", }, { + id: "123e4567-e89b-42d3-a456-556642440053", providerId: response.id, title: "Microsoft Excel", description: "Excel with this A-Z Microsoft Excel Course. Microsoft Excel 2010, 2013, 2016, Excel 2019 and Microsoft/Office 365/2023", @@ -143,6 +147,7 @@ async function main() { author: "Kyle Pew", startDate: new Date("2024-05-01").toISOString(), }, { + id: "123e4567-e89b-42d3-a456-556642440054", providerId: provider1.id, title: "Learn DevOps & Kubernetes", description: "This course enables anyone to get started with devops engineering.", @@ -163,6 +168,7 @@ async function main() { verificationStatus: CourseVerificationStatus.ACCEPTED, cqfScore: 10, }, { + id: "123e4567-e89b-42d3-a456-556642440055", providerId: provider1.id, title: "Introduction to Programming", description: "This course covers all the fundamentals of programming", @@ -179,6 +185,7 @@ async function main() { author: "James Franco", verificationStatus: CourseVerificationStatus.PENDING, }, { + id: "123e4567-e89b-42d3-a456-556642440056", providerId: provider1.id, title: "Introduction to Compiler Engineering", description: "This course covers how compilers are built and also teaches you about how to create custom programming languages", @@ -197,6 +204,7 @@ async function main() { verificationStatus: CourseVerificationStatus.REJECTED, rejectionReason: "Level associated with LLVM is wrong" }, { + id: "123e4567-e89b-42d3-a456-556642440057", providerId: provider1.id, title: "Introduction to Compiler Engineering 2", description: "This course covers how compilers are built and also teaches you about how to create custom programming languages", @@ -234,29 +242,35 @@ async function main() { }, }); - const resp = await prisma.course.findMany({}); - console.log("All courses: ", resp); + // const resp = await prisma.course.findMany({}); + // console.log("All courses: ", resp); const response3 = await prisma.userCourse.createMany({ data: [{ - userId: "c2cc3f08-b6fc-4d53-aa91-2bfcb4e0a5c1", + userId: "123e4567-e89b-42d3-a456-556642440000", feedback: "Great course", rating: 4, - courseId: 1 + courseId: "123e4567-e89b-42d3-a456-556642440050" }, { - userId: "8d1f5e46-4e0d-401e-83b4-5a72fbd6c5a9", + userId: "123e4567-e89b-42d3-a456-556642440000", + courseId: "123e4567-e89b-42d3-a456-556642440051" + }, { + userId: "123e4567-e89b-42d3-a456-556642440001", feedback: "Instructor is very friendly", rating: 4, - courseId: 2 + courseId: "123e4567-e89b-42d3-a456-556642440050" + }, { + userId: "123e4567-e89b-42d3-a456-556642440001", + courseId: "123e4567-e89b-42d3-a456-556642440051" }, { - userId: "a3a5f480-9ac1-4e20-b0d9-7b3a662e2c36", + userId: "123e4567-e89b-42d3-a456-556642440001", feedback: "Some more real world applications could be discussed", rating: 3, - courseId: 2 + courseId: "123e4567-e89b-42d3-a456-556642440052" }, { - userId: "f9b69f4b-1095-4d29-9f49-8f653eb5b3bd", + userId: "123e4567-e89b-42d3-a456-556642440002", feedback: "Not satisfied with the content", rating: 2, - courseId: 3 + courseId: "123e4567-e89b-42d3-a456-556642440052" }] }) console.log(response) diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index be9868b..2f8b201 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -244,7 +244,7 @@ export class AdminController { @ApiResponse({ status: HttpStatus.OK, type: AdminCourseResponse}) @Get('/courses/:courseId') async getCourseById ( - @Param("courseId", ParseIntPipe) courseId: number, @Res() res + @Param("courseId", ParseUUIDPipe) courseId: string, @Res() res ){ try { this.logger.log(`Getting course information for id ${courseId}`); @@ -273,7 +273,7 @@ export class AdminController { @ApiResponse({ status: HttpStatus.OK, type: AdminCourseResponse}) @Patch('/courses/:courseId/accept') async acceptCourse ( - @Param("courseId", ParseIntPipe) courseId: number, @Body() verifyBody: CourseVerify, @Res() res + @Param("courseId", ParseUUIDPipe) courseId: string, @Body() verifyBody: CourseVerify, @Res() res ) { try { this.logger.log(`Verifying the course with id ${courseId}`); @@ -301,7 +301,7 @@ export class AdminController { @ApiResponse({ status: HttpStatus.OK, type: AdminCourseResponse}) @Patch('/courses/:courseId/reject') async rejectCourse ( - @Param("courseId", ParseIntPipe) courseId: number, @Body() courseRejectionRequestDto: RejectProviderRequestDto, @Res() res + @Param("courseId", ParseUUIDPipe) courseId: string, @Body() courseRejectionRequestDto: RejectProviderRequestDto, @Res() res ) { try { this.logger.log(`Processing reject request of course with id ${courseId}`); @@ -329,7 +329,7 @@ export class AdminController { @ApiResponse({ status: HttpStatus.OK, type: AdminCourseResponse}) @Delete('/courses/:courseId') async removeCourse ( - @Param("courseId", ParseIntPipe) courseId: number, @Res() res + @Param("courseId", ParseUUIDPipe) courseId: string, @Res() res ) { try { this.logger.log(`Processing removal request of course with id ${courseId}`); diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index ffc0b46..142ee2e 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -51,25 +51,25 @@ export class AdminService { } // find course by Id - async findCourseById(courseId: number): Promise { + async findCourseById(courseId: string): Promise { return this.courseService.getCourse(courseId); } // accept a course along with the cqf score - async acceptCourse(courseId: number, cqf_score?: number) { + async acceptCourse(courseId: string, cqf_score?: number) { return this.courseService.acceptCourse(courseId, cqf_score); } // reject a course - async rejectCourse(courseId: number, rejectionReason: string) { + async rejectCourse(courseId: string, rejectionReason: string) { return this.courseService.rejectCourse(courseId, rejectionReason); } // remove a course from marketplace - async removeCourse(courseId: number) { + async removeCourse(courseId: string) { return this.courseService.removeCourse(courseId); } diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index 544ff90..a595088 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -50,7 +50,7 @@ export class CourseController { @Get("/:courseId") // Fetch details of one course async getCourse( - @Param("courseId", ParseIntPipe) courseId: number, + @Param("courseId", ParseUUIDPipe) courseId: string, @Res() res ) { try { @@ -80,7 +80,7 @@ export class CourseController { @Post("/:courseId/purchase") // Confirmation of user purchase of a course async purchaseCourse( - @Param("courseId", ParseIntPipe) courseId: number, + @Param("courseId", ParseUUIDPipe) courseId: string, @Body() purchaseDto: PurchaseDto, @Res() res @@ -114,7 +114,7 @@ export class CourseController { @Patch("/:courseId/feedback/:userId") // Give feedback and rating async feedback( - @Param("courseId", ParseIntPipe) courseId: number, + @Param("courseId", ParseUUIDPipe) courseId: string, @Param("userId", ParseUUIDPipe) userId: string, @Body() feedbackDto: FeedbackDto, @Res() res diff --git a/src/course/course.service.ts b/src/course/course.service.ts index fce1ac3..be0294b 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -103,7 +103,7 @@ export class CourseService { }); } - async addPurchaseRecord(courseId: number, purchaseDto: PurchaseDto) { + async addPurchaseRecord(courseId: string, purchaseDto: PurchaseDto) { // Validate course const course = await this.getCourse(courseId); @@ -136,7 +136,7 @@ export class CourseService { const walletPurchaseBody: WalletPurchaseDto = { providerId: course.providerId, credits: course.credits, - description: purchaseDto.transactionDescription + description: `Purchased course ${course.title}` } const walletResponse = await axios.post(process.env.WALLET_SERVICE_URL + endpoint, walletPurchaseBody); return walletResponse.data.data.transaction.transactionId; @@ -154,7 +154,7 @@ export class CourseService { } } - async changeStatus(courseId: number, providerId: string, courseStatusDto: CourseStatusDto) { + async changeStatus(courseId: string, providerId: string, courseStatusDto: CourseStatusDto) { // Validate course const course = await this.getCourse(courseId) @@ -169,7 +169,7 @@ export class CourseService { }); } - async editCourse(courseId: number, editCourseDto: EditCourseDto) { + async editCourse(courseId: string, editCourseDto: EditCourseDto) { // update the course details as required and change its verification status to pending return this.prisma.course.update({ @@ -181,7 +181,7 @@ export class CourseService { }); } - async getCourse(courseId: number): Promise { + async getCourse(courseId: string): Promise { // Find course by ID and throw error if not found const course = await this.prisma.course.findUnique({ @@ -207,7 +207,7 @@ export class CourseService { } } - async getNumOfCourseUsers(courseId: number) { + async getNumOfCourseUsers(courseId: string) { return this.prisma.userCourse.count({ where: { @@ -216,7 +216,7 @@ export class CourseService { }) } - async getCourseByConsumer(courseId: number): Promise { + async getCourseByConsumer(courseId: string): Promise { // Find course by ID and throw error if not found const course = await this.getCourse(courseId); @@ -236,7 +236,7 @@ export class CourseService { } } - async giveCourseFeedback(courseId: number, userId: string, feedbackDto: FeedbackDto) { + async giveCourseFeedback(courseId: string, userId: string, feedbackDto: FeedbackDto) { // Validate course const course = await this.getCourse(courseId); @@ -289,7 +289,7 @@ export class CourseService { }); } - async deleteCourse(courseId: number) { + async deleteCourse(courseId: string) { // Delete the course entry from db await this.prisma.course.delete({ @@ -313,7 +313,7 @@ export class CourseService { }) } - async getPurchasedUsersByCourseId(courseId: number) { + async getPurchasedUsersByCourseId(courseId: string) { // Get all users that have bought a course return this.prisma.userCourse.findMany({ @@ -359,7 +359,7 @@ export class CourseService { }); } - async acceptCourse(courseId: number, cqf_score?: number) { + async acceptCourse(courseId: string, cqf_score?: number) { // Validate course let course = await this.getCourse(courseId); @@ -378,7 +378,7 @@ export class CourseService { }); } - async rejectCourse(courseId: number, rejectionReason: string) { + async rejectCourse(courseId: string, rejectionReason: string) { // Validate course const course = await this.getCourse(courseId); @@ -397,7 +397,7 @@ export class CourseService { }); } - async removeCourse(courseId: number) { + async removeCourse(courseId: string) { // Validate course await this.getCourse(courseId); diff --git a/src/course/dto/completion.dto.ts b/src/course/dto/completion.dto.ts index 61f18d2..93032fc 100644 --- a/src/course/dto/completion.dto.ts +++ b/src/course/dto/completion.dto.ts @@ -7,8 +7,8 @@ export class CompleteCourseDto { // course ID @ApiProperty() @IsNotEmpty() - @IsInt() - courseId: number; + @IsUUID() + courseId: string; // user ID @ApiProperty() diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index d02a4e6..1e13aa2 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -3,7 +3,7 @@ import { JsonValue } from "@prisma/client/runtime/library"; export class CourseResponse { - readonly id: number; + readonly id: string; readonly providerId: string; readonly title: string; readonly description: string; diff --git a/src/course/dto/purchase.dto.ts b/src/course/dto/purchase.dto.ts index 41aa6c8..39cb67d 100644 --- a/src/course/dto/purchase.dto.ts +++ b/src/course/dto/purchase.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsInt, IsNotEmpty, IsString, IsUUID } from "class-validator"; +import { IsNotEmpty, IsUUID } from "class-validator"; export class PurchaseDto { @@ -8,12 +8,6 @@ export class PurchaseDto { @IsNotEmpty() @IsUUID() consumerId: string; - - // Purchase description - @ApiProperty() - @IsNotEmpty() - @IsString() - transactionDescription: string; } export class PurchaseResponseDto { diff --git a/src/course/dto/transaction.dto.ts b/src/course/dto/transaction.dto.ts index e4224aa..f39875f 100644 --- a/src/course/dto/transaction.dto.ts +++ b/src/course/dto/transaction.dto.ts @@ -3,7 +3,7 @@ export class CourseTransactionDto { // course ID - readonly courseId: number; + readonly courseId: string; // course name readonly courseName: string; diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index 4d18891..626f464 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Put, Res } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Put, Res, UploadedFile } from '@nestjs/common'; import { ProviderService } from './provider.service'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SignupDto, SignupResponseDto } from './dto/signup.dto'; @@ -60,13 +60,14 @@ export class ProviderController { // create a new provider account async createAccount( @Body() signupDto: SignupDto, + @UploadedFile() file, @Res() res ) { try { this.logger.log(`Creating new provider account`); - + console.log(file) const providerId = await this.providerService.createNewAccount(signupDto); - + this.logger.log(`successfully created new provider account`); res.status(HttpStatus.CREATED).json({ @@ -184,7 +185,7 @@ export class ProviderController { // edit course information async editCourse( @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, + @Param("courseId", ParseIntPipe) courseId: string, @Body() editCourseDto: EditCourseDto, @Res() res ) { @@ -215,7 +216,7 @@ export class ProviderController { // change course status (archived/unarchived) async changeCourseStatus( @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, + @Param("courseId", ParseIntPipe) courseId: string, @Body() courseStatusDto: CourseStatusDto, @Res() res ) { @@ -277,7 +278,7 @@ export class ProviderController { // remove an existing course async removeCourse( @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, + @Param("courseId", ParseIntPipe) courseId: string, @Res() res ) { try { @@ -338,7 +339,7 @@ export class ProviderController { // View Course Feedback & ratings, numberOfPurchases async getCourseFeedback( @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: number, + @Param("courseId", ParseIntPipe) courseId: string, @Res() res ) { try { diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 986a686..a041fc1 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -18,6 +18,7 @@ import axios from 'axios'; import { UpdatePasswordDto } from './dto/update-password.dto'; import { CourseStatusDto } from 'src/course/dto/course-status.dto'; import { ProviderSettlementDto } from 'src/admin/dto/provider-settlement.dto'; +import * as minio from 'minio'; @Injectable() @@ -43,6 +44,9 @@ export class ProviderService { // Hashing the password const hashedPassword = await this.authService.hashPassword(signupDto.password); + // upload the image to minio + + // Create an entry in the database provider = await this.prisma.provider.create({ data: { @@ -161,7 +165,7 @@ export class ProviderService { return clone; } - async removeCourse(providerId: string, courseId: number) { + async removeCourse(providerId: string, courseId: string) { // Validate course ID provided const course = await this.courseService.getCourse(courseId); @@ -180,7 +184,7 @@ export class ProviderService { return this.courseService.getProviderCourses(providerId); } - async editCourse(providerId: string, courseId: number, editCourseDto: EditCourseDto) { + async editCourse(providerId: string, courseId: string, editCourseDto: EditCourseDto) { // Validate provider await this.getProvider(providerId); @@ -188,7 +192,7 @@ export class ProviderService { return this.courseService.editCourse(courseId, editCourseDto); } - async changeCourseStatus(providerId: string, courseId: number, courseStatusDto: CourseStatusDto) { + async changeCourseStatus(providerId: string, courseId: string, courseStatusDto: CourseStatusDto) { // Validate provider await this.getProvider(providerId); @@ -196,7 +200,7 @@ export class ProviderService { return this.courseService.changeStatus(courseId, providerId, courseStatusDto); } - async getCourseFeedbacks(providerId: string, courseId: number): Promise { + async getCourseFeedbacks(providerId: string, courseId: string): Promise { // Fetch course const course = await this.courseService.getCourse(courseId); diff --git a/src/utils/minio.ts b/src/utils/minio.ts new file mode 100644 index 0000000..89ecf90 --- /dev/null +++ b/src/utils/minio.ts @@ -0,0 +1,34 @@ +import * as Minio from 'minio'; + +// Replace these values with your Minio server details +const minioClient = new Minio.Client({ + endPoint: 'http://10.212.3.229', + port: 9000, + useSSL: false, + accessKey: 'E7lmj73hampENjtjaS85', + secretKey: 'JS8T9GhvCR8k8yRLcyJoFRxhjbv9ys2o46ZblC7S', +}); + +// Replace these values with your bucket and object details +const bucketName = 'bucket1'; +const objectName = 'file1.txt'; +const filePath = 'path/to/your/local/file.txt'; + +// Function to upload a file to Minio +async function uploadFile(objectName: string, filePath: string) { + try { + // Check if the bucket exists, if not, create it + const exists = await minioClient.bucketExists(bucketName); + if (!exists) { + await minioClient.makeBucket(bucketName, 'us-east-1'); + } + + // Upload the file to the specified bucket and object + await minioClient.fPutObject(bucketName, objectName, filePath); + + console.log('File uploaded successfully!'); + } catch (error) { + console.error('Error uploading file:', error); + } +} + From ede21d35b5bac6b3fed727273596c2742372af07 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Thu, 7 Dec 2023 17:07:36 +0530 Subject: [PATCH 57/88] minio and onest changes --- env-example | 8 +- package-lock.json | 10 ++ package.json | 1 + prisma/schema.prisma | 2 +- prisma/seed.ts | 16 +-- src/admin/admin.controller.ts | 71 +++++++++++- src/admin/admin.module.ts | 3 +- src/admin/admin.service.ts | 45 +++++++- src/admin/dto/login.dto.ts | 23 ++++ src/admin/dto/signup.dto.ts | 33 ++++++ src/course/course.controller.ts | 10 +- src/course/course.service.ts | 101 ++++++++---------- src/course/dto/add-course.dto.ts | 5 - src/course/dto/course-response.dto.ts | 1 - src/course/dto/course-status.dto.ts | 2 +- .../dto/provider-profile-response.dto.ts | 2 +- src/provider/dto/signup.dto.ts | 5 - src/provider/dto/update-profile.dto.ts | 6 -- src/provider/provider.controller.ts | 24 +++-- src/provider/provider.service.ts | 38 +++++-- src/utils/minio.ts | 28 +++-- 21 files changed, 302 insertions(+), 132 deletions(-) create mode 100644 src/admin/dto/login.dto.ts create mode 100644 src/admin/dto/signup.dto.ts diff --git a/env-example b/env-example index eddeb12..7295d47 100644 --- a/env-example +++ b/env-example @@ -8,4 +8,10 @@ DATABASE_PASSWORD= DATABASE_NAME= DATABASE_PORT=5432 DATABASE_URL= -WALLET_SERVICE_URL= \ No newline at end of file +WALLET_SERVICE_URL= + +# MINIO +MINIO_ACCESS_KEY= +MINIO_SECRET_KEY= +MINIO_ENDPOINT= +MINIO_BUCKET_NAME= \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c4465ef..2139230 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "@types/express": "^4.17.13", "@types/jest": "28.1.8", "@types/lodash": "^4.14.200", + "@types/multer": "^1.4.11", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", @@ -2251,6 +2252,15 @@ "integrity": "sha512-Ys+/St+2VF4+xuY6+kDIXGxbNRO0mesVg0bbxEfB97Od1Vjpjx9KD1qxs64Gcb3CWPirk9Xe+PT4YiiHQ9T+eg==", "dev": true }, + "node_modules/@types/multer": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.11.tgz", + "integrity": "sha512-svK240gr6LVWvv3YGyhLlA+6LRRWA4mnGIU7RcNmgjBYFl6665wcXrRfxGp5tEPVHUNm5FMcmq7too9bxCwX/w==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "16.18.57", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.57.tgz", diff --git a/package.json b/package.json index 3cf534b..c291ebc 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@types/express": "^4.17.13", "@types/jest": "28.1.8", "@types/lodash": "^4.14.200", + "@types/multer": "^1.4.11", "@types/node": "^16.0.0", "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^5.0.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0c409e4..e47e800 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -43,6 +43,7 @@ enum CourseProgressStatus { model Admin { id String @id @default(uuid()) name String + image String? email String @unique password String } @@ -72,7 +73,6 @@ model Course { courseLink String imgLink String credits Int - noOfLessons Int? language String[] competency Json author String diff --git a/prisma/seed.ts b/prisma/seed.ts index 690f47a..d309f92 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -86,7 +86,6 @@ async function main() { courseLink: "https://www.udemy.com/course/nestjs-the-complete-developers-guide/", imgLink: "https://courses.nestjs.com/img/logo.svg", credits: 4, - noOfLessons: 3, language: ["en"], competency: { "API Development": ["Level1", "Level2"], @@ -105,7 +104,6 @@ async function main() { courseLink: "https://www.udemy.com/course/graphic-design-masterclass-everything-you-need-to-know/", imgLink: "https://www.unite.ai/wp-content/uploads/2023/05/emily-bernal-v9vII5gV8Lw-unsplash.jpg", credits: 5, - noOfLessons: 3, language: ["en"], competency: { "Photoshop": ["Level2", "Level3"], @@ -123,7 +121,6 @@ async function main() { courseLink: "https://www.udemy.com/course/python-for-data-science-and-machine-learning-bootcamp/", imgLink: "https://blog.imarticus.org/wp-content/uploads/2021/12/learn-Python-for-data-science.jpg", credits: 2, - noOfLessons: 3, language: ["en"], competency: { "Statistics": ["Level1"], @@ -139,7 +136,6 @@ async function main() { courseLink: "https://www.udemy.com/course/microsoft-excel-2013-from-beginner-to-advanced-and-beyond/", imgLink: "https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Microsoft_Excel_2013-2019_logo.svg/587px-Microsoft_Excel_2013-2019_logo.svg.png", credits: 4, - noOfLessons: 3, language: ["en"], competency: { "Excel": ["Level1", "Level2", "Level3", "Level4"] @@ -154,7 +150,6 @@ async function main() { courseLink: "https://udemy.com/courses/pYUxbhj", imgLink: "https://udemy.com/courses/pYUxbhj/images/cover1.jpg", credits: 120, - noOfLessons: 120, language: ["english", "hindi"], competency: { "Docker": ["Level1", "Level3"], @@ -175,7 +170,6 @@ async function main() { courseLink: "https://udemy.com/courses/jQKsLpm", imgLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, - noOfLessons: 100, language: ["english", "hindi"], avgRating: 3.5, competency: { @@ -192,7 +186,6 @@ async function main() { courseLink: "https://udemy.com/courses/jQKsLpm", imgLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, - noOfLessons: 100, language: ["english", "hindi"], competency: { "Compiler Design": ["Level2", "Level3"], @@ -211,7 +204,6 @@ async function main() { courseLink: "https://udemy.com/courses/jQKsLpm", imgLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, - noOfLessons: 100, language: ["english", "hindi"], competency: { "Compiler Design": ["Level2", "Level3"], @@ -224,12 +216,13 @@ async function main() { status: CourseStatus.ARCHIVED }] }) - + const hashedPassword1 = await bcrypt.hash("asdfghjkl", saltRounds); const admin = await prisma.admin.create({ data: { name: "Sanchit Uke", email: "sanchit@esmagico.in", - password: "asdfghjkl", + password: hashedPassword1, + image: "https://avatars.githubusercontent.com/u/46641520?v=4", id: "123e4567-e89b-42d3-a456-556642440020", } }); @@ -238,7 +231,8 @@ async function main() { data: { name: 'admin1', email: "admin1@gmail.com", - password: "123456", + image: "https://avatars.githubusercontent.com/u/46641520?v=4", + password: hashedPassword, }, }); diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 2f8b201..92cb0fc 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Body, Get, Post, Patch, Res, Delete, HttpStatus, Param, ParseIntPipe, Logger, ParseUUIDPipe} from '@nestjs/common'; +import { Controller, Body, Get, Post, Patch, Res, Delete, HttpStatus, Param, Logger, ParseUUIDPipe, UseInterceptors, UploadedFile} from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; import { AdminService } from './admin.service'; import { ProviderProfileResponse } from '../provider/dto/provider-profile-response.dto'; @@ -14,6 +14,9 @@ import { ProviderVerify } from './dto/provider-verify-response.dto'; import { RejectProviderResponseDto } from './dto/reject-provider-response.dto'; import { RejectProviderRequestDto } from './dto/reject-provider-request.dto'; import { AdminCourseResponse } from 'src/course/dto/course-response.dto'; +import { AdminSignupDto } from './dto/signup.dto'; +import { AdminLoginDto, AdminLoginResponseDto } from './dto/login.dto'; +import { FileInterceptor } from '@nestjs/platform-express'; @Controller('admin') @ApiTags('admin') @@ -22,6 +25,72 @@ export class AdminController { constructor(private adminService: AdminService) {} + @ApiOperation({ summary: 'signup for admin' }) + @ApiResponse({ status: HttpStatus.OK, type: AdminLoginResponseDto }) + @Post("/signup") + @UseInterceptors(FileInterceptor('image')) + // admin signup + async adminSignup( + @Body() signupDto: AdminSignupDto, + @UploadedFile() image: Express.Multer.File, + @Res() res + ) { + try { + this.logger.log(`Signing up as admin`) + + const adminId = await this.adminService.signup(signupDto, image); + + this.logger.log(`Successfully signed up as admin`) + + res.status(HttpStatus.OK).json({ + message: "sign up successful", + data: { + adminId + } + }); + } catch (err) { + this.logger.error(`Failed to sign up the admin with the given credentials`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to sign up as admin", + }); + } + } + + @ApiOperation({ summary: 'Login for admin' }) + @ApiResponse({ status: HttpStatus.OK, type: AdminLoginResponseDto }) + @Post("/login") + // admin login + async login( + @Body() loginDto: AdminLoginDto, + @Res() res + ) { + try { + this.logger.log(`Logging in as admin`) + + const adminId = await this.adminService.login(loginDto); + + this.logger.log(`Successfully logged in as admin`) + + res.status(HttpStatus.OK).json({ + message: "login successful", + data: { + adminId + } + }); + } catch (err) { + this.logger.error(`Failed to login the admin with the given credentials`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to login as admin", + }); + } + } + @ApiOperation({ summary: "Get all providers" }) @ApiResponse({ status: HttpStatus.OK, type: ProviderProfileResponse, isArray: true}) @Get('/providers') diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts index 78bdeb5..353dca0 100644 --- a/src/admin/admin.module.ts +++ b/src/admin/admin.module.ts @@ -6,9 +6,10 @@ import { PrismaModule } from 'nestjs-prisma'; import { MockWalletModule } from 'src/mock-wallet/mock-wallet.module'; import { CourseModule } from 'src/course/course.module'; import { ProviderModule } from 'src/provider/provider.module'; +import { AuthModule } from 'src/auth/auth.module'; @Module({ - imports: [PrismaModule, CourseModule, MockWalletModule, ProviderModule], + imports: [PrismaModule, CourseModule, MockWalletModule, ProviderModule, AuthModule], controllers: [AdminController], providers: [AdminService, PrismaService], exports: [AdminService] diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 142ee2e..e00b52f 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -1,5 +1,5 @@ -import { HttpException, Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, HttpException, Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { Provider, Course } from '@prisma/client'; import { EditProvider } from './dto/edit-provider.dto'; @@ -10,6 +10,10 @@ import axios from 'axios'; import { AdminCourseResponse } from 'src/course/dto/course-response.dto'; import { ProviderSettlementDto } from './dto/provider-settlement.dto'; import { ProviderProfileResponse } from 'src/provider/dto/provider-profile-response.dto'; +import { AdminSignupDto } from './dto/signup.dto'; +import { uploadFile } from 'src/utils/minio'; +import { AuthService } from 'src/auth/auth.service'; +import { AdminLoginDto } from './dto/login.dto'; @Injectable() export class AdminService { @@ -18,9 +22,46 @@ export class AdminService { private prisma: PrismaService, private wallet: MockWalletService, private providerService: ProviderService, - private courseService: CourseService + private courseService: CourseService, + private authService: AuthService ) {} + // create a new admin + async signup(signupDto: AdminSignupDto, image?: Express.Multer.File) { + + let imageUrl: string | undefined; + if(image) { + imageUrl = await uploadFile(signupDto.name, image.buffer, `/admin`) + } + + // Hashing the password + const hashedPassword = await this.authService.hashPassword(signupDto.password); + + const admin = await this.prisma.admin.create({ + data: { + name: signupDto.name, + email: signupDto.email, + password: hashedPassword, + image: imageUrl + } + }); + return admin.id; + } + + async login(loginDto: AdminLoginDto) { + const admin = await this.prisma.admin.findUnique({ + where: { email: loginDto.email } + }); + if (admin == null) { + throw new NotFoundException(`Admin not found`); + } + const isMatch = await this.authService.comparePasswords(loginDto.password, admin.password); + if (!isMatch) { + throw new BadRequestException(`Invalid credentials`); + } + return admin.id; + } + // verify provider account async verifyProvider(providerId: string) { diff --git a/src/admin/dto/login.dto.ts b/src/admin/dto/login.dto.ts new file mode 100644 index 0000000..b67cef9 --- /dev/null +++ b/src/admin/dto/login.dto.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEmail, IsNotEmpty, IsString } from "class-validator"; + +export class AdminLoginDto { + + // email ID + @ApiProperty() + @IsNotEmpty() + @IsEmail() + email: string + + // password + @ApiProperty() + @IsNotEmpty() + @IsString() + password: string +} + +export class AdminLoginResponseDto { + + // provider ID + readonly adminId: number +} \ No newline at end of file diff --git a/src/admin/dto/signup.dto.ts b/src/admin/dto/signup.dto.ts new file mode 100644 index 0000000..4ced12c --- /dev/null +++ b/src/admin/dto/signup.dto.ts @@ -0,0 +1,33 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsEmail, IsNotEmpty, IsString, IsStrongPassword } from "class-validator"; + +export class AdminSignupDto { + + // name + @ApiProperty() + @IsNotEmpty() + @IsString() + name: string + + // email ID + @ApiProperty() + @IsNotEmpty() + @IsEmail() + email: string + + // password + @ApiProperty() + // A strong password should have a minimum length of 8 characters, + // at least 1 uppercase letter, 1 lowercase letter, 1 number, and 1 special symbol. + @IsNotEmpty({ message: 'Password is required' }) + @IsStrongPassword( + { + minLength: 8, + minUppercase: 1, + minNumbers: 1, + minSymbols: 1, + }, + { message: 'Password is not strong enough' }, + ) + password: string +} \ No newline at end of file diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index a595088..bc3ed73 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -77,26 +77,22 @@ export class CourseController { @ApiOperation({ summary: 'Confirmation of user purchase of a course' }) @ApiResponse({ status: HttpStatus.OK, type: PurchaseResponseDto }) - @Post("/:courseId/purchase") + @Post("/:courseId/purchase/:consumerId") // Confirmation of user purchase of a course async purchaseCourse( @Param("courseId", ParseUUIDPipe) courseId: string, - @Body() purchaseDto: PurchaseDto, - + @Param("consumerId", ParseUUIDPipe) consumerId: string, @Res() res ) { try { this.logger.log(`Recording the user purchase of the course`); - const transactionId = await this.courseService.addPurchaseRecord(courseId, purchaseDto); + await this.courseService.addPurchaseRecord(courseId, consumerId); this.logger.log(`Successfully recorded the purchase`); res.status(HttpStatus.OK).json({ message: "purchase successful", - data: { - walletTransactionId: transactionId - } }) } catch (err) { this.logger.error(`Failed to record the purchase`); diff --git a/src/course/course.service.ts b/src/course/course.service.ts index be0294b..7e94c20 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -2,7 +2,7 @@ import { NotFoundException, BadRequestException, Injectable, NotAcceptableExcept import { PrismaService } from "../prisma/prisma.service"; import { FeedbackDto } from "./dto/feedback.dto"; import { AddCourseDto } from "./dto/add-course.dto"; -import { CourseProgressStatus, CourseStatus, CourseVerificationStatus } from "@prisma/client"; +import { CourseProgressStatus, CourseStatus, CourseVerificationStatus, Provider } from "@prisma/client"; import { CompleteCourseDto } from "./dto/completion.dto"; import { EditCourseDto } from "./dto/edit-course.dto"; import { AdminCourseResponse, CourseResponse, ProviderCourseResponse } from "src/course/dto/course-response.dto"; @@ -10,7 +10,8 @@ import { CourseTransactionDto } from "./dto/transaction.dto"; import { SearchResponseDTO } from "./dto/search-response.dto"; import { CourseStatusDto } from "./dto/course-status.dto"; import axios from "axios"; -import { PurchaseDto, WalletPurchaseDto } from "./dto/purchase.dto"; +import { uploadFile } from "src/utils/minio"; +import { ProviderProfileResponse } from "src/provider/dto/provider-profile-response.dto"; @Injectable() export class CourseService { @@ -18,7 +19,7 @@ export class CourseService { private prisma: PrismaService, ) {} - async searchCourses(searchInput: string): Promise { + async searchCourses(searchInput: string): Promise { // Searches for the courses available in the DB that match or contain the input search string // in their title, author, description or competency @@ -65,45 +66,49 @@ export class CourseService { && (c.startDate ? c.startDate <= new Date(): true) && (c.endDate ? c.endDate >= new Date(): true) ); - // courses = courses.map((c) => { - // let {cqfScore, impactScore, verificationStatus, rejectionReason, provider, ...clone} = c; - // const courseResponse: CourseResponse = { - // ...clone, - // providerName: provider.orgName - // } - // return courseResponse; - // }); - return courses.map((course) => { - return { - id: course.id.toString(), - title: course.title, - long_desc: course.description, - provider_id: course.providerId, - provider_name: course.provider.orgName, - price: course.credits.toString(), - languages: course.language, - competency: course.competency, - imgUrl: course.imgLink, - rating: course.avgRating?.toString() || "0", - startTime: new Date().toISOString(), // need to change - endTime: new Date().toISOString(), // need to change - noOfPurchases: course._count.userCourses, + return courses.map((c) => { + let {cqfScore, impactScore, verificationStatus, rejectionReason, provider, _count, ...clone} = c; + const courseResponse: CourseResponse = { + ...clone, + providerName: provider.orgName, + numOfUsers: _count.userCourses } + return courseResponse; }); + // return courses.map((course) => { + // return { + // id: course.id.toString(), + // title: course.title, + // long_desc: course.description, + // provider_id: course.providerId, + // provider_name: course.provider.orgName, + // price: course.credits.toString(), + // languages: course.language, + // competency: course.competency, + // imgUrl: course.imgLink, + // rating: course.avgRating?.toString() || "0", + // startTime: new Date().toISOString(), // need to change + // endTime: new Date().toISOString(), // need to change + // noOfPurchases: course._count.userCourses, + // } + // }); } - async addCourse(providerId: string, addCourseDto: AddCourseDto) { + async addCourse(addCourseDto: AddCourseDto, provider: ProviderProfileResponse, image: Express.Multer.File) { + + const imgLink = await uploadFile( addCourseDto.title, image.buffer, `/provider/${provider.orgName}`); // add new course to the platform return await this.prisma.course.create({ data: { - providerId, - ...addCourseDto + providerId: provider.id, + ...addCourseDto, + imgLink, } }); } - async addPurchaseRecord(courseId: string, purchaseDto: PurchaseDto) { + async addPurchaseRecord(courseId: string, consumerId: string) { // Validate course const course = await this.getCourse(courseId); @@ -117,7 +122,7 @@ export class CourseService { // Check if course already purchased const record = await this.prisma.userCourse.findFirst({ - where: { userId: purchaseDto.consumerId, courseId: courseId } + where: { userId: consumerId, courseId: courseId } }); if(record != null) throw new BadRequestException("Course already purchased by the user"); @@ -126,32 +131,10 @@ export class CourseService { // create new record for purchase await this.prisma.userCourse.create({ data: { - userId: purchaseDto.consumerId, + userId: consumerId, courseId, } }); - // forward to wallet service for transaction - try { - const endpoint = `/api/consumers/${purchaseDto.consumerId}/purchase`; - const walletPurchaseBody: WalletPurchaseDto = { - providerId: course.providerId, - credits: course.credits, - description: `Purchased course ${course.title}` - } - const walletResponse = await axios.post(process.env.WALLET_SERVICE_URL + endpoint, walletPurchaseBody); - return walletResponse.data.data.transaction.transactionId; - } catch (err) { - // if transaction failed, delete the record - await this.prisma.userCourse.delete({ - where: { - userId_courseId: { - userId: purchaseDto.consumerId, - courseId - } - } - }); - throw new HttpException(err.response || "Wallet service not running", err.response?.status || err.status || 500) - } } async changeStatus(courseId: string, providerId: string, courseStatusDto: CourseStatusDto) { @@ -169,7 +152,14 @@ export class CourseService { }); } - async editCourse(courseId: string, editCourseDto: EditCourseDto) { + async editCourse(courseId: string, editCourseDto: EditCourseDto, provider: ProviderProfileResponse, image?: Express.Multer.File) { + + // Validate course + const course = await this.getCourse(courseId); + let imgUrl = course.imgLink; + if(image) { + imgUrl = await uploadFile( editCourseDto.title ?? course.title, image.buffer, `/provider/${provider.orgName}`); + } // update the course details as required and change its verification status to pending return this.prisma.course.update({ @@ -177,6 +167,7 @@ export class CourseService { data: { ...editCourseDto, verificationStatus: CourseVerificationStatus.PENDING, + imgLink: imgUrl } }); } diff --git a/src/course/dto/add-course.dto.ts b/src/course/dto/add-course.dto.ts index 913a8bc..d3aa5d4 100644 --- a/src/course/dto/add-course.dto.ts +++ b/src/course/dto/add-course.dto.ts @@ -23,11 +23,6 @@ export class AddCourseDto { @IsUrl() courseLink: string; - // course image - @ApiProperty() - @IsNotEmpty() - @IsUrl() - imgLink: string; // number of credits required to purchase course @ApiProperty() diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index 1e13aa2..1bbd773 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -10,7 +10,6 @@ export class CourseResponse { readonly courseLink: string; readonly imgLink: string; readonly credits: number; - readonly noOfLessons: number | null; readonly language: string[]; readonly competency: JsonValue; readonly author: string; diff --git a/src/course/dto/course-status.dto.ts b/src/course/dto/course-status.dto.ts index f7abdc5..d0f406c 100644 --- a/src/course/dto/course-status.dto.ts +++ b/src/course/dto/course-status.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { CourseStatus } from "@prisma/client"; -import { IsString, IsOptional, IsNotEmpty, IsEnum } from "class-validator"; +import { IsNotEmpty, IsEnum } from "class-validator"; export class CourseStatusDto { diff --git a/src/provider/dto/provider-profile-response.dto.ts b/src/provider/dto/provider-profile-response.dto.ts index bfee470..c3db65e 100644 --- a/src/provider/dto/provider-profile-response.dto.ts +++ b/src/provider/dto/provider-profile-response.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { ProviderStatus } from "@prisma/client"; import { JsonValue } from "@prisma/client/runtime/library"; -import { IsEmail, IsEnum, IsObject, IsOptional as IsNotEmpty, IsString, IsUUID, IsOptional, IsUrl, IsPhoneNumber, IsDate } from 'class-validator'; +import { IsEmail, IsEnum, IsObject, IsString, IsUUID, IsOptional, IsUrl, IsPhoneNumber, IsDate, IsNotEmpty } from 'class-validator'; export class ProviderProfileResponse { @ApiProperty({required: false}) diff --git a/src/provider/dto/signup.dto.ts b/src/provider/dto/signup.dto.ts index b31729a..6667014 100644 --- a/src/provider/dto/signup.dto.ts +++ b/src/provider/dto/signup.dto.ts @@ -38,11 +38,6 @@ export class SignupDto { @IsString() orgName: string; - // organisation logo image link - @ApiProperty() - @IsNotEmpty() - @IsUrl() - orgLogo: string; // phone number @ApiProperty() diff --git a/src/provider/dto/update-profile.dto.ts b/src/provider/dto/update-profile.dto.ts index dc3f7fa..b744154 100644 --- a/src/provider/dto/update-profile.dto.ts +++ b/src/provider/dto/update-profile.dto.ts @@ -22,12 +22,6 @@ export class UpdateProfileDto { @IsOptional() orgName?: string - // organisation logo image link - @ApiProperty() - @IsString() - @IsOptional() - orgLogo?: string - // phone number @ApiProperty() @IsPhoneNumber() diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index 626f464..7f060d8 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Put, Res, UploadedFile } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Put, Res, UploadedFile, UseInterceptors } from '@nestjs/common'; import { ProviderService } from './provider.service'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SignupDto, SignupResponseDto } from './dto/signup.dto'; @@ -14,6 +14,7 @@ import { ProviderProfileResponse } from './dto/provider-profile-response.dto'; import { getPrismaErrorStatusAndMessage } from 'src/utils/utils'; import { UpdatePasswordDto } from './dto/update-password.dto'; import { CourseStatusDto } from 'src/course/dto/course-status.dto'; +import { FileInterceptor } from '@nestjs/platform-express'; @Controller('provider') @ApiTags('provider') @@ -57,17 +58,18 @@ export class ProviderController { @ApiOperation({ summary: 'create provider account' }) @ApiResponse({ status: HttpStatus.CREATED, type: SignupResponseDto }) @Post("/signup") + @UseInterceptors(FileInterceptor('logo')) // create a new provider account async createAccount( @Body() signupDto: SignupDto, - @UploadedFile() file, + @UploadedFile() logo: Express.Multer.File, @Res() res ) { try { this.logger.log(`Creating new provider account`); - console.log(file) - const providerId = await this.providerService.createNewAccount(signupDto); - + + const providerId = await this.providerService.createNewAccount(signupDto, logo); + this.logger.log(`successfully created new provider account`); res.status(HttpStatus.CREATED).json({ @@ -152,16 +154,18 @@ export class ProviderController { @ApiOperation({ summary: 'update provider profile information' }) @ApiResponse({ status: HttpStatus.OK }) @Put("/:providerId/profile") + @UseInterceptors(FileInterceptor('logo')) // update provider profile information async updateProfile( @Param("providerId", ParseUUIDPipe) providerId: string, @Body() updateProfileDto: UpdateProfileDto, + @UploadedFile() logo: Express.Multer.File, @Res() res ) { try { this.logger.log(`Updating provider profile`); - await this.providerService.updateProfileInfo(providerId, updateProfileDto); + await this.providerService.updateProfileInfo(providerId, updateProfileDto, logo); this.logger.log(`successfully updated provider profile`); @@ -182,17 +186,19 @@ export class ProviderController { @ApiOperation({ summary: 'edit course information' }) @ApiResponse({ status: HttpStatus.OK }) @Patch("/:providerId/course/:courseId") + @UseInterceptors(FileInterceptor('image')) // edit course information async editCourse( @Param("providerId", ParseUUIDPipe) providerId: string, @Param("courseId", ParseIntPipe) courseId: string, + @UploadedFile() image: Express.Multer.File, @Body() editCourseDto: EditCourseDto, @Res() res ) { try { this.logger.log(`Updating course information`); - await this.providerService.editCourse(providerId, courseId, editCourseDto); + await this.providerService.editCourse(providerId, courseId, editCourseDto, image); this.logger.log(`Successfully updated course information`); @@ -244,16 +250,18 @@ export class ProviderController { @ApiOperation({ summary: 'add new course' }) @ApiResponse({ status: HttpStatus.CREATED, type: ProviderCourseResponse }) @Post("/:providerId/course") + @UseInterceptors(FileInterceptor('image')) // add new course async addCourse( @Param("providerId", ParseUUIDPipe) providerId: string, @Body() addCourseDto: AddCourseDto, + @UploadedFile() image: Express.Multer.File, @Res() res ) { try { this.logger.log(`Adding new course`); - const course = await this.providerService.addNewCourse(providerId, addCourseDto); + const course = await this.providerService.addNewCourse(providerId, addCourseDto, image); this.logger.log(`Successfully added new course`); diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index a041fc1..c30cc07 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -18,7 +18,7 @@ import axios from 'axios'; import { UpdatePasswordDto } from './dto/update-password.dto'; import { CourseStatusDto } from 'src/course/dto/course-status.dto'; import { ProviderSettlementDto } from 'src/admin/dto/provider-settlement.dto'; -import * as minio from 'minio'; +import { uploadFile } from 'src/utils/minio'; @Injectable() @@ -29,7 +29,7 @@ export class ProviderService { private authService: AuthService ) {} - async createNewAccount(signupDto: SignupDto) { + async createNewAccount(signupDto: SignupDto, logo: Express.Multer.File) { // Check if email already exists let provider = await this.prisma.provider.findUnique({ @@ -44,8 +44,11 @@ export class ProviderService { // Hashing the password const hashedPassword = await this.authService.hashPassword(signupDto.password); + if(!logo) + throw new BadRequestException("Logo not uploaded"); + // upload the image to minio - + const imgLink = await uploadFile("logo", logo.buffer, `/provider/${signupDto.orgName}/`) // Create an entry in the database provider = await this.prisma.provider.create({ @@ -55,7 +58,7 @@ export class ProviderService { password: hashedPassword, paymentInfo: signupDto.paymentInfo, orgName: signupDto.orgName, - orgLogo: signupDto.orgLogo, + orgLogo: imgLink, phone: signupDto.phone } }); @@ -132,13 +135,23 @@ export class ProviderService { } // Used when provider makes a request to update profile - async updateProfileInfo(providerId: string, updateProfileDto: UpdateProfileDto) { + async updateProfileInfo(providerId: string, updateProfileDto: UpdateProfileDto, logo?: Express.Multer.File) { + // Fetch provider + const provider = await this.getProvider(providerId); + let imgLink = provider.orgLogo; + if(logo) { + // upload the image to minio + imgLink = await uploadFile(provider.orgName, logo.buffer, `/provider/${provider.orgName}/`) + } await this.prisma.provider.update({ where: { id: providerId }, - data: updateProfileDto + data: { + ...updateProfileDto, + orgLogo: imgLink + } }) } @@ -151,7 +164,7 @@ export class ProviderService { }); } - async addNewCourse(providerId: string, addCourseDto: AddCourseDto): Promise { + async addNewCourse(providerId: string, addCourseDto: AddCourseDto, image: Express.Multer.File): Promise { // Fetch provider const provider = await this.getProvider(providerId); @@ -160,8 +173,11 @@ export class ProviderService { if(provider.status != ProviderStatus.VERIFIED) throw new UnauthorizedException("Provider account is not verified"); + if(!image) + throw new BadRequestException("Image not uploaded"); + // Forward to course service - const {cqfScore, impactScore, ...clone} = await this.courseService.addCourse(providerId, addCourseDto); + const {cqfScore, impactScore, ...clone} = await this.courseService.addCourse(addCourseDto, provider, image); return clone; } @@ -184,12 +200,12 @@ export class ProviderService { return this.courseService.getProviderCourses(providerId); } - async editCourse(providerId: string, courseId: string, editCourseDto: EditCourseDto) { + async editCourse(providerId: string, courseId: string, editCourseDto: EditCourseDto, image?: Express.Multer.File) { // Validate provider - await this.getProvider(providerId); + const provider = await this.getProvider(providerId); - return this.courseService.editCourse(courseId, editCourseDto); + return this.courseService.editCourse(courseId, editCourseDto, provider, image); } async changeCourseStatus(providerId: string, courseId: string, courseStatusDto: CourseStatusDto) { diff --git a/src/utils/minio.ts b/src/utils/minio.ts index 89ecf90..3c4f644 100644 --- a/src/utils/minio.ts +++ b/src/utils/minio.ts @@ -1,34 +1,32 @@ import * as Minio from 'minio'; +const endPoint = process.env.MINIO_ENDPOINT!; + // Replace these values with your Minio server details const minioClient = new Minio.Client({ - endPoint: 'http://10.212.3.229', + endPoint: endPoint, port: 9000, useSSL: false, - accessKey: 'E7lmj73hampENjtjaS85', - secretKey: 'JS8T9GhvCR8k8yRLcyJoFRxhjbv9ys2o46ZblC7S', + accessKey: process.env.MINIO_ACCESS_KEY!, + secretKey: process.env.MINIO_SECRET_KEY!, }); // Replace these values with your bucket and object details -const bucketName = 'bucket1'; -const objectName = 'file1.txt'; -const filePath = 'path/to/your/local/file.txt'; +const bucketName = process.env.MINIO_BUCKET_NAME!; // Function to upload a file to Minio -async function uploadFile(objectName: string, filePath: string) { - try { - // Check if the bucket exists, if not, create it +export async function uploadFile(objectName: string, fileBuffer: Buffer, path: string) { + + // Check if the bucket exists, if not, create it const exists = await minioClient.bucketExists(bucketName); if (!exists) { - await minioClient.makeBucket(bucketName, 'us-east-1'); + throw new Error("Bucket not found") } // Upload the file to the specified bucket and object - await minioClient.fPutObject(bucketName, objectName, filePath); - + await minioClient.putObject(bucketName, objectName, fileBuffer); console.log('File uploaded successfully!'); - } catch (error) { - console.error('Error uploading file:', error); - } + + return `https://${endPoint}/${bucketName}${path}/${objectName}` } From b9ec84c4eebcf062dcbf5139740883a7e1d0750f Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Thu, 7 Dec 2023 17:35:14 +0530 Subject: [PATCH 58/88] login name and image --- src/admin/admin.controller.ts | 12 ++++++++---- src/admin/admin.service.ts | 4 ++-- src/admin/dto/login.dto.ts | 2 ++ 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 92cb0fc..9a52bce 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -38,14 +38,16 @@ export class AdminController { try { this.logger.log(`Signing up as admin`) - const adminId = await this.adminService.signup(signupDto, image); + const admin = await this.adminService.signup(signupDto, image); this.logger.log(`Successfully signed up as admin`) res.status(HttpStatus.OK).json({ message: "sign up successful", data: { - adminId + admin: admin.id, + name: admin.name, + image: admin.image } }); } catch (err) { @@ -70,14 +72,16 @@ export class AdminController { try { this.logger.log(`Logging in as admin`) - const adminId = await this.adminService.login(loginDto); + const admin = await this.adminService.login(loginDto); this.logger.log(`Successfully logged in as admin`) res.status(HttpStatus.OK).json({ message: "login successful", data: { - adminId + admin: admin.id, + name: admin.name, + image: admin.image } }); } catch (err) { diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index e00b52f..5b60b12 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -45,7 +45,7 @@ export class AdminService { image: imageUrl } }); - return admin.id; + return admin; } async login(loginDto: AdminLoginDto) { @@ -59,7 +59,7 @@ export class AdminService { if (!isMatch) { throw new BadRequestException(`Invalid credentials`); } - return admin.id; + return admin; } // verify provider account diff --git a/src/admin/dto/login.dto.ts b/src/admin/dto/login.dto.ts index b67cef9..93a1b59 100644 --- a/src/admin/dto/login.dto.ts +++ b/src/admin/dto/login.dto.ts @@ -20,4 +20,6 @@ export class AdminLoginResponseDto { // provider ID readonly adminId: number + readonly name: string + readonly image: string } \ No newline at end of file From bcd96758a2415bd6a895f90ea471ef0a029fb982 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Thu, 7 Dec 2023 18:23:13 +0530 Subject: [PATCH 59/88] seed changes --- prisma/seed.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index d309f92..6149c39 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,4 +1,4 @@ -import { CourseStatus, CourseVerificationStatus, PrismaClient, ProviderStatus } from '@prisma/client' +import { CourseProgressStatus, CourseStatus, CourseVerificationStatus, PrismaClient, ProviderStatus } from '@prisma/client' import * as bcrypt from 'bcrypt'; const prisma = new PrismaClient() @@ -243,14 +243,19 @@ async function main() { userId: "123e4567-e89b-42d3-a456-556642440000", feedback: "Great course", rating: 4, + status: CourseProgressStatus.COMPLETED, + courseCompletionScore: 100, courseId: "123e4567-e89b-42d3-a456-556642440050" }, { userId: "123e4567-e89b-42d3-a456-556642440000", - courseId: "123e4567-e89b-42d3-a456-556642440051" + courseId: "123e4567-e89b-42d3-a456-556642440051", + status: CourseProgressStatus.COMPLETED, }, { userId: "123e4567-e89b-42d3-a456-556642440001", feedback: "Instructor is very friendly", rating: 4, + status: CourseProgressStatus.COMPLETED, + courseCompletionScore: 100, courseId: "123e4567-e89b-42d3-a456-556642440050" }, { userId: "123e4567-e89b-42d3-a456-556642440001", @@ -259,11 +264,15 @@ async function main() { userId: "123e4567-e89b-42d3-a456-556642440001", feedback: "Some more real world applications could be discussed", rating: 3, + status: CourseProgressStatus.COMPLETED, + courseCompletionScore: 100, courseId: "123e4567-e89b-42d3-a456-556642440052" }, { userId: "123e4567-e89b-42d3-a456-556642440002", feedback: "Not satisfied with the content", rating: 2, + status: CourseProgressStatus.COMPLETED, + courseCompletionScore: 100, courseId: "123e4567-e89b-42d3-a456-556642440052" }] }) From 5be5d15d22b62bbea552f4bbe3844dbc6b9c3d14 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 8 Dec 2023 01:07:56 +0530 Subject: [PATCH 60/88] json , image link fix --- env-example | 1 + src/admin/admin.service.ts | 3 +- src/course/course.service.ts | 47 +++++- src/course/dto/add-course.dto.ts | 10 +- src/course/dto/completion.dto.ts | 9 +- src/course/dto/edit-course.dto.ts | 3 +- src/provider/dto/signup.dto.ts | 8 +- src/provider/dto/update-profile.dto.ts | 4 +- src/provider/provider.controller.ts | 208 ++++++++++++------------- src/provider/provider.service.ts | 15 +- 10 files changed, 171 insertions(+), 137 deletions(-) diff --git a/env-example b/env-example index 7295d47..475c9ed 100644 --- a/env-example +++ b/env-example @@ -9,6 +9,7 @@ DATABASE_NAME= DATABASE_PORT=5432 DATABASE_URL= WALLET_SERVICE_URL= +MARKETPLACE_PORTAL_URL= # MINIO MINIO_ACCESS_KEY= diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 5b60b12..e0e5b54 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -31,7 +31,8 @@ export class AdminService { let imageUrl: string | undefined; if(image) { - imageUrl = await uploadFile(signupDto.name, image.buffer, `/admin`) + const imageName = signupDto.name.replace(" ", "_") + imageUrl = await uploadFile(imageName, image.buffer, `/admin`) } // Hashing the password diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 7e94c20..ff9cafc 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -96,13 +96,17 @@ export class CourseService { async addCourse(addCourseDto: AddCourseDto, provider: ProviderProfileResponse, image: Express.Multer.File) { - const imgLink = await uploadFile( addCourseDto.title, image.buffer, `/provider/${provider.orgName}`); + const imageName = addCourseDto.title.replace(" ", "_") + const imgLink = await uploadFile( imageName, image.buffer, `/provider/${provider.orgName}`); + const {competency, ...clone} = addCourseDto; + // add new course to the platform return await this.prisma.course.create({ data: { providerId: provider.id, - ...addCourseDto, + ...clone, + competency: JSON.parse(competency), imgLink, } }); @@ -158,14 +162,17 @@ export class CourseService { const course = await this.getCourse(courseId); let imgUrl = course.imgLink; if(image) { - imgUrl = await uploadFile( editCourseDto.title ?? course.title, image.buffer, `/provider/${provider.orgName}`); + const imageName = (editCourseDto.title ?? course.title).replace(" ", "_") + imgUrl = await uploadFile( imageName, image.buffer, `/provider/${provider.orgName}`); } - + const {competency, ...clone} = editCourseDto; + // update the course details as required and change its verification status to pending return this.prisma.course.update({ where: { id: courseId }, data: { - ...editCourseDto, + ...clone, + competency: competency ? JSON.parse(competency) : undefined, verificationStatus: CourseVerificationStatus.PENDING, imgLink: imgUrl } @@ -316,17 +323,43 @@ export class CourseService { async markCourseComplete(completeCourseDto: CompleteCourseDto) { + // Validate course + const userCourse = await this.prisma.userCourse.findUnique({ + where: { + userId_courseId: { + userId: completeCourseDto.userId, + courseId: completeCourseDto.courseId + } + } + }); + if(!userCourse) + throw new NotFoundException("User has not purchased this course"); + + // Forward to marketplace portal + const uri = process.env.MARKETPLACE_PORTAL_URL; + if(!uri) + throw new HttpException("Marketplace URL not set", 500); + + const endpoint = `/api/consumer/${completeCourseDto.userId}/course/complete`; + const courseIdDto = { + courseId: completeCourseDto.courseId, + } + await axios.patch(uri + endpoint, courseIdDto); + // Update a course as complete for a purchased course await this.prisma.userCourse.update({ where: { userId_courseId: { - ...completeCourseDto + courseId: completeCourseDto.courseId, + userId: completeCourseDto.userId, } }, data: { - status: CourseProgressStatus.COMPLETED + status: CourseProgressStatus.COMPLETED, + courseCompletionScore: completeCourseDto.courseCompletionScore } }) + } async fetchAllCourses() : Promise { diff --git a/src/course/dto/add-course.dto.ts b/src/course/dto/add-course.dto.ts index d3aa5d4..2bf650e 100644 --- a/src/course/dto/add-course.dto.ts +++ b/src/course/dto/add-course.dto.ts @@ -1,7 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { CourseStatus } from "@prisma/client"; import { ArrayNotEmpty, IsArray, IsDate, IsEnum, IsInt, IsNotEmpty, IsNumber, IsOptional, IsString, IsUrl, Min } from "class-validator"; -import { CompetencyMap } from "src/utils/types"; export class AddCourseDto { @@ -31,12 +30,6 @@ export class AddCourseDto { @IsInt() credits: number; - // Number of lessons - @ApiProperty() - @IsInt() - @IsOptional() - noOfLessons?: number; - // language @ApiProperty() @IsArray() @@ -46,7 +39,8 @@ export class AddCourseDto { // competency @ApiProperty() @IsNotEmpty() - competency: CompetencyMap; + @IsString() + competency: string; // author @ApiProperty() diff --git a/src/course/dto/completion.dto.ts b/src/course/dto/completion.dto.ts index 93032fc..acb6eec 100644 --- a/src/course/dto/completion.dto.ts +++ b/src/course/dto/completion.dto.ts @@ -1,5 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsInt, IsNotEmpty, IsUUID } from "class-validator"; +import { IsInt, IsNotEmpty, IsNumber, IsUUID, Min } from "class-validator"; export class CompleteCourseDto { @@ -15,4 +15,11 @@ export class CompleteCourseDto { @IsNotEmpty() @IsUUID() userId: string; + + // Course completion score + @ApiProperty() + @IsNotEmpty() + @IsNumber() + @Min(0) + courseCompletionScore: number; } \ No newline at end of file diff --git a/src/course/dto/edit-course.dto.ts b/src/course/dto/edit-course.dto.ts index 43ccdee..754af5e 100644 --- a/src/course/dto/edit-course.dto.ts +++ b/src/course/dto/edit-course.dto.ts @@ -50,7 +50,8 @@ export class EditCourseDto { // competency @ApiProperty() @IsOptional() - competency?: CompetencyMap; + @IsString() + competency?: string; // author @ApiProperty() diff --git a/src/provider/dto/signup.dto.ts b/src/provider/dto/signup.dto.ts index 6667014..301b88a 100644 --- a/src/provider/dto/signup.dto.ts +++ b/src/provider/dto/signup.dto.ts @@ -1,6 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsEmail, IsNotEmpty, IsObject, IsOptional, IsPhoneNumber, IsString, IsStrongPassword, IsUrl } from "class-validator"; -import { PaymentInfo } from "src/utils/types"; +import { IsEmail, IsNotEmpty, IsOptional, IsPhoneNumber, IsString, IsStrongPassword } from "class-validator"; export class SignupDto { @@ -38,7 +37,6 @@ export class SignupDto { @IsString() orgName: string; - // phone number @ApiProperty() @IsNotEmpty() @@ -48,8 +46,8 @@ export class SignupDto { // payment info @ApiProperty() @IsOptional() - @IsObject() - paymentInfo?: PaymentInfo + @IsString() + paymentInfo?: string } export class SignupResponseDto { diff --git a/src/provider/dto/update-profile.dto.ts b/src/provider/dto/update-profile.dto.ts index b744154..7a66eed 100644 --- a/src/provider/dto/update-profile.dto.ts +++ b/src/provider/dto/update-profile.dto.ts @@ -31,6 +31,6 @@ export class UpdateProfileDto { // payment info @ApiProperty() @IsOptional() - @IsObject() - paymentInfo?: PaymentInfo + @IsString() + paymentInfo?: string } diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index 7f060d8..f0f7795 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -183,251 +183,251 @@ export class ProviderController { } } - @ApiOperation({ summary: 'edit course information' }) - @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:providerId/course/:courseId") + @ApiOperation({ summary: 'add new course' }) + @ApiResponse({ status: HttpStatus.CREATED, type: ProviderCourseResponse }) + @Post("/:providerId/course") @UseInterceptors(FileInterceptor('image')) - // edit course information - async editCourse( + // add new course + async addCourse( @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: string, + @Body() addCourseDto: AddCourseDto, @UploadedFile() image: Express.Multer.File, - @Body() editCourseDto: EditCourseDto, @Res() res ) { try { - this.logger.log(`Updating course information`); + this.logger.log(`Adding new course`); - await this.providerService.editCourse(providerId, courseId, editCourseDto, image); + const course = await this.providerService.addNewCourse(providerId, addCourseDto, image); - this.logger.log(`Successfully updated course information`); + this.logger.log(`Successfully added new course`); - res.status(HttpStatus.OK).json({ - message: "course edited successfully", + res.status(HttpStatus.CREATED).json({ + message: "course added successfully", + data: course }) } catch (err) { - this.logger.error(`Failed to update course information`); + this.logger.error(`Failed to add the course`); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ statusCode, - message: errorMessage || "Failed to update course information`", + message: errorMessage || "Failed to add the course`", }); } } - @ApiOperation({ summary: 'change course status' }) - @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:providerId/course/:courseId/status") - // change course status (archived/unarchived) - async changeCourseStatus( + @ApiOperation({ summary: 'View courses offered by self' }) + @ApiResponse({ status: HttpStatus.OK, type: [ProviderCourseResponse] }) + @Get("/:providerId/course") + // View courses offered by self + async fetchProviderCourses( @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: string, - @Body() courseStatusDto: CourseStatusDto, @Res() res ) { try { - this.logger.log(`Changing course status`); + this.logger.log(`Getting courses`); - await this.providerService.changeCourseStatus(providerId, courseId, courseStatusDto); + const courses = await this.providerService.getCourses(providerId); - this.logger.log(`Successfully changed course status`); + this.logger.log(`Successfully retrieved the courses`); res.status(HttpStatus.OK).json({ - message: "course status changed successfully", + message: "courses fetched successfully", + data: courses }) } catch (err) { - this.logger.error(`Failed to change course status`); + this.logger.error(`Failed to fetch the courses`); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ statusCode, - message: errorMessage || "Failed to change course status`", + message: errorMessage || "Failed to fetch the courses", }); } } - - @ApiOperation({ summary: 'add new course' }) - @ApiResponse({ status: HttpStatus.CREATED, type: ProviderCourseResponse }) - @Post("/:providerId/course") - @UseInterceptors(FileInterceptor('image')) - // add new course - async addCourse( + + @ApiOperation({ summary: 'Get transactions of all courses' }) + @ApiResponse({ status: HttpStatus.OK, type: [CourseTransactionDto] }) + @Get("/:providerId/course/transactions") + // Get transactions of all courses + async getCourseTransactions( @Param("providerId", ParseUUIDPipe) providerId: string, - @Body() addCourseDto: AddCourseDto, - @UploadedFile() image: Express.Multer.File, @Res() res ) { try { - this.logger.log(`Adding new course`); + this.logger.log(`Getting course transactions`); - const course = await this.providerService.addNewCourse(providerId, addCourseDto, image); + const transactionsResponse = await this.providerService.getCourseTransactions(providerId); - this.logger.log(`Successfully added new course`); + this.logger.log(`Successfully retrieved course transactions`); - res.status(HttpStatus.CREATED).json({ - message: "course added successfully", - data: course + res.status(HttpStatus.OK).json({ + message: "transactions fetched successfully", + data: transactionsResponse }) } catch (err) { - this.logger.error(`Failed to add the course`); + this.logger.error(`Failed to fetch the transactions`); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ statusCode, - message: errorMessage || "Failed to add the course`", + message: errorMessage || "Failed to fetch the transactions", }); } } - @ApiOperation({ summary: 'remove a course' }) + @ApiOperation({ summary: 'Mark course as complete' }) @ApiResponse({ status: HttpStatus.OK }) - @Delete("/:providerId/course/:courseId") - // remove an existing course - async removeCourse( + @Patch("/:providerId/course/completion") + // Mark course as complete for a user + async markCourseComplete( @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: string, + @Body() completeCourseDto: CompleteCourseDto, @Res() res ) { try { - this.logger.log(`Removing course`); - - await this.providerService.removeCourse(providerId, courseId); + this.logger.log(`Updating course as complete`); - this.logger.log(`Successfully deleted the course`); + await this.providerService.markCourseComplete(providerId, completeCourseDto); + this.logger.log(`Successfully marked the course as complete`); res.status(HttpStatus.OK).json({ - message: "course deleted successfully", + message: "course marked complete", }) } catch (err) { - this.logger.error(`Failed to delete the course`); + this.logger.error(`Failed to mark the course completion`); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ statusCode, - message: errorMessage || "Failed to delete the course`", + message: errorMessage || "Failed to mark the course completion", }); } } - @ApiOperation({ summary: 'View courses offered by self' }) - @ApiResponse({ status: HttpStatus.OK, type: [ProviderCourseResponse] }) - @Get("/:providerId/course") - // View courses offered by self - async fetchProviderCourses( + @ApiOperation({ summary: 'edit course information' }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:providerId/course/:courseId") + @UseInterceptors(FileInterceptor('image')) + // edit course information + async editCourse( @Param("providerId", ParseUUIDPipe) providerId: string, + @Param("courseId", ParseUUIDPipe) courseId: string, + @UploadedFile() image: Express.Multer.File, + @Body() editCourseDto: EditCourseDto, @Res() res ) { try { - this.logger.log(`Getting courses`); + this.logger.log(`Updating course information`); - const courses = await this.providerService.getCourses(providerId); + await this.providerService.editCourse(providerId, courseId, editCourseDto, image); - this.logger.log(`Successfully retrieved the courses`); + this.logger.log(`Successfully updated course information`); res.status(HttpStatus.OK).json({ - message: "courses fetched successfully", - data: courses + message: "course edited successfully", }) } catch (err) { - this.logger.error(`Failed to fetch the courses`); + this.logger.error(`Failed to update course information`); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ statusCode, - message: errorMessage || "Failed to fetch the courses", + message: errorMessage || "Failed to update course information`", }); } } - @ApiOperation({ summary: 'View Course Feedback & ratings, numberOfPurchases' }) - @ApiResponse({ status: HttpStatus.OK, type: FeedbackResponseDto }) - @Get("/:providerId/course/:courseId/feedback") - // View Course Feedback & ratings, numberOfPurchases - async getCourseFeedback( + @ApiOperation({ summary: 'remove a course' }) + @ApiResponse({ status: HttpStatus.OK }) + @Delete("/:providerId/course/:courseId") + // remove an existing course + async removeCourse( @Param("providerId", ParseUUIDPipe) providerId: string, @Param("courseId", ParseIntPipe) courseId: string, @Res() res ) { try { - this.logger.log(`Getting course feedbacks`); + this.logger.log(`Removing course`); - const feedbackResponse = await this.providerService.getCourseFeedbacks(providerId, courseId); + await this.providerService.removeCourse(providerId, courseId); + + this.logger.log(`Successfully deleted the course`); - this.logger.log(`Successfully retrieved the feedbacks`); res.status(HttpStatus.OK).json({ - message: "feedbacks fetched successfully", - data: feedbackResponse + message: "course deleted successfully", }) } catch (err) { - this.logger.error(`Failed to fetch the feedbacks`); + this.logger.error(`Failed to delete the course`); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ statusCode, - message: errorMessage || "Failed to fetch the feedbacks", + message: errorMessage || "Failed to delete the course`", }); } } - @ApiOperation({ summary: 'Get transactions of all courses' }) - @ApiResponse({ status: HttpStatus.OK, type: [CourseTransactionDto] }) - @Get("/:providerId/course/transactions") - // Get transactions of all courses - async getCourseTransactions( + @ApiOperation({ summary: 'change course status' }) + @ApiResponse({ status: HttpStatus.OK }) + @Patch("/:providerId/course/:courseId/status") + // change course status (archived/unarchived) + async changeCourseStatus( @Param("providerId", ParseUUIDPipe) providerId: string, + @Param("courseId", ParseIntPipe) courseId: string, + @Body() courseStatusDto: CourseStatusDto, @Res() res ) { try { - this.logger.log(`Getting course transactions`); + this.logger.log(`Changing course status`); - const transactionsResponse = await this.providerService.getCourseTransactions(providerId); + await this.providerService.changeCourseStatus(providerId, courseId, courseStatusDto); - this.logger.log(`Successfully retrieved course transactions`); + this.logger.log(`Successfully changed course status`); res.status(HttpStatus.OK).json({ - message: "transactions fetched successfully", - data: transactionsResponse + message: "course status changed successfully", }) } catch (err) { - this.logger.error(`Failed to fetch the transactions`); + this.logger.error(`Failed to change course status`); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ statusCode, - message: errorMessage || "Failed to fetch the transactions", + message: errorMessage || "Failed to change course status`", }); } } - @ApiOperation({ summary: 'Mark course as complete' }) - @ApiResponse({ status: HttpStatus.OK }) - @Patch("/:providerId/course/completion") - // Mark course as complete for a user - async markCourseComplete( + @ApiOperation({ summary: 'View Course Feedback & ratings, numberOfPurchases' }) + @ApiResponse({ status: HttpStatus.OK, type: FeedbackResponseDto }) + @Get("/:providerId/course/:courseId/feedback") + // View Course Feedback & ratings, numberOfPurchases + async getCourseFeedback( @Param("providerId", ParseUUIDPipe) providerId: string, - @Body() completeCourseDto: CompleteCourseDto, + @Param("courseId", ParseIntPipe) courseId: string, @Res() res ) { try { - this.logger.log(`Updating course as complete`); + this.logger.log(`Getting course feedbacks`); - await this.providerService.markCourseComplete(providerId, completeCourseDto); + const feedbackResponse = await this.providerService.getCourseFeedbacks(providerId, courseId); - this.logger.log(`Successfully marked the course as complete`); + this.logger.log(`Successfully retrieved the feedbacks`); res.status(HttpStatus.OK).json({ - message: "course marked complete", + message: "feedbacks fetched successfully", + data: feedbackResponse }) } catch (err) { - this.logger.error(`Failed to mark the course completion`); + this.logger.error(`Failed to fetch the feedbacks`); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ statusCode, - message: errorMessage || "Failed to mark the course completion", + message: errorMessage || "Failed to fetch the feedbacks", }); } } diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index c30cc07..cd790b4 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -142,14 +142,16 @@ export class ProviderService { let imgLink = provider.orgLogo; if(logo) { // upload the image to minio - imgLink = await uploadFile(provider.orgName, logo.buffer, `/provider/${provider.orgName}/`) + imgLink = await uploadFile("logo", logo.buffer, `/provider/${provider.orgName}/`) } + const { paymentInfo, ...clone } = updateProfileDto; await this.prisma.provider.update({ where: { id: providerId }, data: { - ...updateProfileDto, + ...clone, + paymentInfo: paymentInfo ? JSON.parse(paymentInfo) : undefined, orgLogo: imgLink } }) @@ -259,12 +261,9 @@ export class ProviderService { if(course.providerId != providerId) throw new BadRequestException("Course does not belong to this provider"); - // Forward to course service. Error is thrown when user has not purchased a course - try { - await this.courseService.markCourseComplete(completeCourseDto); - } catch { - throw new NotFoundException("This user has not subscribed to this course"); - } + // Forward to course service + await this.courseService.markCourseComplete(completeCourseDto); + } async fetchAllProviders(): Promise { From 63edb50988d4701d6deb62eeab1e77530f65d262 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 8 Dec 2023 10:42:04 +0530 Subject: [PATCH 61/88] admin create wallet --- src/admin/admin.service.ts | 20 ++++++++++++++++++++ src/provider/provider.service.ts | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index e0e5b54..0a15ff5 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -46,6 +46,26 @@ export class AdminService { image: imageUrl } }); + try { + // Forward to wallet service for creation of wallet + if(!process.env.WALLET_SERVICE_URL) + throw new HttpException("Wallet service URL not defined", 500); + const url = process.env.WALLET_SERVICE_URL; + const endpoint = url + `/api/wallet/create`; + const reqBody = { + userId: admin.id, + type: 'ADMIN', + credits: 0 + } + const resp = await axios.post(endpoint, reqBody); + } catch(err) { + await this.prisma.provider.delete({ + where: { + id: admin.id + } + }); + throw new HttpException(err.response || "Wallet service not running", err.response?.status || err.status || 500); + } return admin; } diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index cd790b4..4112991 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -56,7 +56,7 @@ export class ProviderService { name: signupDto.name, email: signupDto.email, password: hashedPassword, - paymentInfo: signupDto.paymentInfo, + paymentInfo: signupDto.paymentInfo ? JSON.parse(signupDto.paymentInfo) : undefined, orgName: signupDto.orgName, orgLogo: imgLink, phone: signupDto.phone From c48c01d6cd46922c66e8bc33c79f57f30da31f8f Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 8 Dec 2023 11:28:02 +0530 Subject: [PATCH 62/88] admin sign up change --- prisma/seed.ts | 1 + src/admin/admin.service.ts | 26 ++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 6149c39..0075242 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -233,6 +233,7 @@ async function main() { email: "admin1@gmail.com", image: "https://avatars.githubusercontent.com/u/46641520?v=4", password: hashedPassword, + id: "123e4567-e89b-42d3-a456-556642440021", }, }); diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 0a15ff5..3968d16 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -19,8 +19,7 @@ import { AdminLoginDto } from './dto/login.dto'; export class AdminService { constructor( - private prisma: PrismaService, - private wallet: MockWalletService, + private prisma: PrismaService, private providerService: ProviderService, private courseService: CourseService, private authService: AuthService @@ -50,6 +49,7 @@ export class AdminService { // Forward to wallet service for creation of wallet if(!process.env.WALLET_SERVICE_URL) throw new HttpException("Wallet service URL not defined", 500); + const url = process.env.WALLET_SERVICE_URL; const endpoint = url + `/api/wallet/create`; const reqBody = { @@ -57,15 +57,33 @@ export class AdminService { type: 'ADMIN', credits: 0 } - const resp = await axios.post(endpoint, reqBody); + await axios.post(endpoint, reqBody); } catch(err) { - await this.prisma.provider.delete({ + await this.prisma.admin.delete({ where: { id: admin.id } }); throw new HttpException(err.response || "Wallet service not running", err.response?.status || err.status || 500); } + try { + // Forward to marketplace portal for creation in marketplace + if(!process.env.MARKETPLACE_PORTAL_URL) + throw new HttpException("Marketplace service URL not defined", 500); + + const url = process.env.MARKETPLACE_PORTAL_URL; + const endpoint = url + `/api/admin/${admin.id}`; + + await axios.post(endpoint); + } catch(err) { + await this.prisma.admin.delete({ + where: { + id: admin.id + } + }); + throw new HttpException(err.response || "Marketplace service not running", err.response?.status || err.status || 500); + } + return admin; } From 00146279c5f45f82c4b0e06f1de039845385d635 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 8 Dec 2023 11:55:30 +0530 Subject: [PATCH 63/88] uuid fix --- src/course/course.controller.ts | 2 +- src/provider/provider.controller.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index bc3ed73..1da501d 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, HttpStatus, Logger, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Query, Res } from "@nestjs/common"; +import { Body, Controller, Get, HttpStatus, Logger, Param, ParseUUIDPipe, Patch, Post, Query, Res } from "@nestjs/common"; import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { CourseService } from "./course.service"; import { FeedbackDto } from "./dto/feedback.dto"; diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index f0f7795..b826d25 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Put, Res, UploadedFile, UseInterceptors } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpStatus, Logger, Param, ParseUUIDPipe, Patch, Post, Put, Res, UploadedFile, UseInterceptors } from '@nestjs/common'; import { ProviderService } from './provider.service'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { SignupDto, SignupResponseDto } from './dto/signup.dto'; @@ -345,7 +345,7 @@ export class ProviderController { // remove an existing course async removeCourse( @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: string, + @Param("courseId", ParseUUIDPipe) courseId: string, @Res() res ) { try { @@ -376,7 +376,7 @@ export class ProviderController { // change course status (archived/unarchived) async changeCourseStatus( @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: string, + @Param("courseId", ParseUUIDPipe) courseId: string, @Body() courseStatusDto: CourseStatusDto, @Res() res ) { @@ -407,7 +407,7 @@ export class ProviderController { // View Course Feedback & ratings, numberOfPurchases async getCourseFeedback( @Param("providerId", ParseUUIDPipe) providerId: string, - @Param("courseId", ParseIntPipe) courseId: string, + @Param("courseId", ParseUUIDPipe) courseId: string, @Res() res ) { try { From a859137a744ec075e8b7f72e14fcff04b3704d2b Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 8 Dec 2023 15:15:46 +0530 Subject: [PATCH 64/88] public image --- prisma/schema.prisma | 4 ++-- prisma/seed.ts | 16 ++++++------- src/admin/admin.service.ts | 2 +- src/course/course.service.ts | 34 ++++++++++++--------------- src/course/dto/course-response.dto.ts | 2 +- src/provider/provider.service.ts | 5 ++-- src/utils/minio.ts | 11 ++++++--- 7 files changed, 38 insertions(+), 36 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e47e800..0c6932d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -66,7 +66,7 @@ model Provider { model Course { - id String @id @default(uuid()) + courseId String @id @default(uuid()) providerId String title String description String @@ -99,7 +99,7 @@ model UserCourse { courseCompletionScore Float? rating Int? feedback String? - course Course @relation(fields: [courseId], references: [id], onDelete: Cascade) + course Course @relation(fields: [courseId], references: [courseId], onDelete: Cascade) @@unique([userId, courseId]) } diff --git a/prisma/seed.ts b/prisma/seed.ts index 0075242..a66a91d 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -79,7 +79,7 @@ async function main() { const response1 = await prisma.course.createMany({ data: [{ - id: "123e4567-e89b-42d3-a456-556642440050", + courseId: "123e4567-e89b-42d3-a456-556642440050", providerId: provider1.id, title: "NestJS Complete", description: "Build full featured backend APIs incredibly quickly with Nest, TypeORM, and Typescript. Includes testing and deployment!", @@ -97,7 +97,7 @@ async function main() { endDate: new Date("2024-07-01").toISOString(), verificationStatus: CourseVerificationStatus.ACCEPTED, }, { - id: "123e4567-e89b-42d3-a456-556642440051", + courseId: "123e4567-e89b-42d3-a456-556642440051", providerId: provider1.id, title: "Graphic Design Masterclass", description: "The Ultimate Graphic Design Course Which Covers Photoshop, Illustrator, InDesign, Design Theory, Branding & Logo Design", @@ -114,7 +114,7 @@ async function main() { endDate: new Date("2024-09-01").toISOString(), verificationStatus: CourseVerificationStatus.ACCEPTED, }, { - id: "123e4567-e89b-42d3-a456-556642440052", + courseId: "123e4567-e89b-42d3-a456-556642440052", providerId: provider1.id, title: "Python for Data Science", description: "Learn how to use NumPy, Pandas, Seaborn , Matplotlib , Plotly , Scikit-Learn , Machine Learning, Tensorflow , and more", @@ -129,7 +129,7 @@ async function main() { }, author: "Jose Portilla", }, { - id: "123e4567-e89b-42d3-a456-556642440053", + courseId: "123e4567-e89b-42d3-a456-556642440053", providerId: response.id, title: "Microsoft Excel", description: "Excel with this A-Z Microsoft Excel Course. Microsoft Excel 2010, 2013, 2016, Excel 2019 and Microsoft/Office 365/2023", @@ -143,7 +143,7 @@ async function main() { author: "Kyle Pew", startDate: new Date("2024-05-01").toISOString(), }, { - id: "123e4567-e89b-42d3-a456-556642440054", + courseId: "123e4567-e89b-42d3-a456-556642440054", providerId: provider1.id, title: "Learn DevOps & Kubernetes", description: "This course enables anyone to get started with devops engineering.", @@ -163,7 +163,7 @@ async function main() { verificationStatus: CourseVerificationStatus.ACCEPTED, cqfScore: 10, }, { - id: "123e4567-e89b-42d3-a456-556642440055", + courseId: "123e4567-e89b-42d3-a456-556642440055", providerId: provider1.id, title: "Introduction to Programming", description: "This course covers all the fundamentals of programming", @@ -179,7 +179,7 @@ async function main() { author: "James Franco", verificationStatus: CourseVerificationStatus.PENDING, }, { - id: "123e4567-e89b-42d3-a456-556642440056", + courseId: "123e4567-e89b-42d3-a456-556642440056", providerId: provider1.id, title: "Introduction to Compiler Engineering", description: "This course covers how compilers are built and also teaches you about how to create custom programming languages", @@ -197,7 +197,7 @@ async function main() { verificationStatus: CourseVerificationStatus.REJECTED, rejectionReason: "Level associated with LLVM is wrong" }, { - id: "123e4567-e89b-42d3-a456-556642440057", + courseId: "123e4567-e89b-42d3-a456-556642440057", providerId: provider1.id, title: "Introduction to Compiler Engineering 2", description: "This course covers how compilers are built and also teaches you about how to create custom programming languages", diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 3968d16..fa7e5c8 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -31,7 +31,7 @@ export class AdminService { let imageUrl: string | undefined; if(image) { const imageName = signupDto.name.replace(" ", "_") - imageUrl = await uploadFile(imageName, image.buffer, `/admin`) + imageUrl = await uploadFile(`admin/${imageName}`, image.buffer) } // Hashing the password diff --git a/src/course/course.service.ts b/src/course/course.service.ts index ff9cafc..b1d663b 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -2,7 +2,7 @@ import { NotFoundException, BadRequestException, Injectable, NotAcceptableExcept import { PrismaService } from "../prisma/prisma.service"; import { FeedbackDto } from "./dto/feedback.dto"; import { AddCourseDto } from "./dto/add-course.dto"; -import { CourseProgressStatus, CourseStatus, CourseVerificationStatus, Provider } from "@prisma/client"; +import { CourseProgressStatus, CourseStatus, CourseVerificationStatus } from "@prisma/client"; import { CompleteCourseDto } from "./dto/completion.dto"; import { EditCourseDto } from "./dto/edit-course.dto"; import { AdminCourseResponse, CourseResponse, ProviderCourseResponse } from "src/course/dto/course-response.dto"; @@ -97,16 +97,14 @@ export class CourseService { async addCourse(addCourseDto: AddCourseDto, provider: ProviderProfileResponse, image: Express.Multer.File) { const imageName = addCourseDto.title.replace(" ", "_") - const imgLink = await uploadFile( imageName, image.buffer, `/provider/${provider.orgName}`); + const imgLink = await uploadFile( `provider/${provider.orgName.replace(" ", "_")}/${imageName}`, image.buffer); - const {competency, ...clone} = addCourseDto; // add new course to the platform return await this.prisma.course.create({ data: { providerId: provider.id, - ...clone, - competency: JSON.parse(competency), + ...addCourseDto, imgLink, } }); @@ -151,7 +149,7 @@ export class CourseService { // update the course status to archived return this.prisma.course.update({ - where: { id: courseId, providerId }, + where: { courseId, providerId }, data: { status: courseStatusDto.status } }); } @@ -163,16 +161,14 @@ export class CourseService { let imgUrl = course.imgLink; if(image) { const imageName = (editCourseDto.title ?? course.title).replace(" ", "_") - imgUrl = await uploadFile( imageName, image.buffer, `/provider/${provider.orgName}`); + imgUrl = await uploadFile( `provider/${provider.orgName.replace(" ", "_")}/${imageName}`, image.buffer); } - const {competency, ...clone} = editCourseDto; // update the course details as required and change its verification status to pending return this.prisma.course.update({ - where: { id: courseId }, + where: { courseId }, data: { - ...clone, - competency: competency ? JSON.parse(competency) : undefined, + ...editCourseDto, verificationStatus: CourseVerificationStatus.PENDING, imgLink: imgUrl } @@ -184,7 +180,7 @@ export class CourseService { // Find course by ID and throw error if not found const course = await this.prisma.course.findUnique({ where: { - id: courseId + courseId }, include: { provider: { @@ -279,7 +275,7 @@ export class CourseService { }); await this.prisma.course.update({ where: { - id: courseId + courseId }, data: { avgRating: avgRating._avg.rating @@ -292,7 +288,7 @@ export class CourseService { // Delete the course entry from db await this.prisma.course.delete({ where: { - id: courseId + courseId } }) } @@ -394,7 +390,7 @@ export class CourseService { } // Update the course as accepted return this.prisma.course.update({ - where: { id: courseId }, + where: { courseId }, data: { verificationStatus: CourseVerificationStatus.ACCEPTED, cqfScore: cqf_score @@ -413,7 +409,7 @@ export class CourseService { } // Reject the course return this.prisma.course.update({ - where: {id: courseId}, + where: { courseId }, data: { verificationStatus: CourseVerificationStatus.REJECTED, rejectionReason: rejectionReason @@ -428,7 +424,7 @@ export class CourseService { // Delete course entry return this.prisma.course.delete({ - where: {id: courseId} + where: { courseId} }); } @@ -440,7 +436,7 @@ export class CourseService { providerId }, select: { - id: true, + courseId: true, title: true, startDate: true, endDate: true, @@ -456,7 +452,7 @@ export class CourseService { // Refactor to the DTO format required return transactions.map((c) => { return { - courseId: c.id, + courseId: c.courseId, courseName: c.title, startDate: c.startDate, endDate: c.endDate, diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index 1bbd773..754d598 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -3,7 +3,7 @@ import { JsonValue } from "@prisma/client/runtime/library"; export class CourseResponse { - readonly id: string; + readonly courseId: string; readonly providerId: string; readonly title: string; readonly description: string; diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 4112991..a24a1b8 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -48,7 +48,7 @@ export class ProviderService { throw new BadRequestException("Logo not uploaded"); // upload the image to minio - const imgLink = await uploadFile("logo", logo.buffer, `/provider/${signupDto.orgName}/`) + const imgLink = await uploadFile(`provider/${signupDto.orgName.replace(" ", "_")}/logo`, logo.buffer) // Create an entry in the database provider = await this.prisma.provider.create({ @@ -142,7 +142,8 @@ export class ProviderService { let imgLink = provider.orgLogo; if(logo) { // upload the image to minio - imgLink = await uploadFile("logo", logo.buffer, `/provider/${provider.orgName}/`) + const imageName = updateProfileDto.orgName ?? provider.orgName; + imgLink = await uploadFile(`provider/${imageName.replace(" ", "_")}/logo`, logo.buffer) } const { paymentInfo, ...clone } = updateProfileDto; await this.prisma.provider.update({ diff --git a/src/utils/minio.ts b/src/utils/minio.ts index 3c4f644..e10752e 100644 --- a/src/utils/minio.ts +++ b/src/utils/minio.ts @@ -15,18 +15,23 @@ const minioClient = new Minio.Client({ const bucketName = process.env.MINIO_BUCKET_NAME!; // Function to upload a file to Minio -export async function uploadFile(objectName: string, fileBuffer: Buffer, path: string) { +export async function uploadFile(objectName: string, fileBuffer: Buffer) { // Check if the bucket exists, if not, create it const exists = await minioClient.bucketExists(bucketName); if (!exists) { throw new Error("Bucket not found") } - + // Upload the file to the specified bucket and object await minioClient.putObject(bucketName, objectName, fileBuffer); console.log('File uploaded successfully!'); - return `https://${endPoint}/${bucketName}${path}/${objectName}` + // minioClient.presignedUrl('GET', bucketName, objectName, 24 * 60 * 60, function (err, presignedUrl) { + // if (err) return console.log(err) + // console.log(presignedUrl) + // }) + + return `https://${endPoint}/${bucketName}/${objectName}` } From 330ef48a6fc0fe1b9c3e4e677e1921b0411890c4 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 8 Dec 2023 16:03:22 +0530 Subject: [PATCH 65/88] competency fix --- src/course/course.service.ts | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/course/course.service.ts b/src/course/course.service.ts index b1d663b..2bb5417 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -67,11 +67,12 @@ export class CourseService { && (c.endDate ? c.endDate >= new Date(): true) ); return courses.map((c) => { - let {cqfScore, impactScore, verificationStatus, rejectionReason, provider, _count, ...clone} = c; + let {cqfScore, impactScore, verificationStatus, rejectionReason, provider, _count, competency, ...clone} = c; const courseResponse: CourseResponse = { ...clone, providerName: provider.orgName, - numOfUsers: _count.userCourses + numOfUsers: _count.userCourses, + competency: (typeof competency == "string") ? JSON.parse(competency) : competency, } return courseResponse; }); @@ -194,9 +195,10 @@ export class CourseService { throw new NotFoundException("Course does not exist"); // let courseResponse: AdminCourseResponse - const { provider, ...courseResponse } = course; + const { provider, competency, ...courseResponse } = course; return { ...courseResponse, + competency: (typeof competency == "string") ? JSON.parse(competency) : competency, providerName: provider.orgName } } @@ -302,8 +304,11 @@ export class CourseService { } }) return courses.map((c) => { - const {cqfScore, impactScore, ...clone} = c; - return clone; + const {cqfScore, impactScore, competency, ...clone} = c; + return { + ...clone, + competency: (typeof competency == "string") ? JSON.parse(competency) : competency, + } }) } @@ -371,9 +376,10 @@ export class CourseService { } }); return courses.map((c) => { - const { provider, ...clone } = c; + const { provider, competency, ...clone } = c; return { ...clone, + competency: (typeof competency == "string") ? JSON.parse(competency) : competency, providerName: provider.orgName } }); @@ -389,13 +395,17 @@ export class CourseService { throw new NotAcceptableException(`Course is either rejected or is already accepted.`); } // Update the course as accepted - return this.prisma.course.update({ + const {competency, ...clone} = await this.prisma.course.update({ where: { courseId }, data: { verificationStatus: CourseVerificationStatus.ACCEPTED, cqfScore: cqf_score } }); + return { + ...clone, + competency: (typeof competency == "string") ? JSON.parse(competency) : competency, + } } async rejectCourse(courseId: string, rejectionReason: string) { @@ -408,13 +418,17 @@ export class CourseService { throw new NotAcceptableException(`Course is already rejected or is accepted`); } // Reject the course - return this.prisma.course.update({ + const {competency, ...clone} = await this.prisma.course.update({ where: { courseId }, data: { verificationStatus: CourseVerificationStatus.REJECTED, rejectionReason: rejectionReason } }); + return { + ...clone, + competency: (typeof competency == "string") ? JSON.parse(competency) : competency, + } } async removeCourse(courseId: string) { @@ -423,9 +437,13 @@ export class CourseService { await this.getCourse(courseId); // Delete course entry - return this.prisma.course.delete({ + const {competency, ...clone} = await this.prisma.course.delete({ where: { courseId} }); + return { + ...clone, + competency: (typeof competency == "string") ? JSON.parse(competency) : competency, + } } async getCourseTransactions(providerId: string): Promise { From cf49cd5d6d7083c5110e524fb9c15c8ff0f7ea79 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 8 Dec 2023 18:57:49 +0530 Subject: [PATCH 66/88] provider logo --- src/course/course.service.ts | 8 ++++++-- src/course/dto/course-response.dto.ts | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 2bb5417..3b1b988 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -187,6 +187,7 @@ export class CourseService { provider: { select: { orgName: true, + orgLogo: true } } } @@ -199,7 +200,8 @@ export class CourseService { return { ...courseResponse, competency: (typeof competency == "string") ? JSON.parse(competency) : competency, - providerName: provider.orgName + providerName: provider.orgName, + providerLogo: provider.orgLogo } } @@ -371,6 +373,7 @@ export class CourseService { provider: { select: { orgName: true, + orgLogo: true } } } @@ -380,7 +383,8 @@ export class CourseService { return { ...clone, competency: (typeof competency == "string") ? JSON.parse(competency) : competency, - providerName: provider.orgName + providerName: provider.orgName, + providerLogo: provider.orgLogo } }); } diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index 754d598..6b081c2 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -32,4 +32,5 @@ export class AdminCourseResponse extends ProviderCourseResponse { readonly cqfScore: number | null; readonly impactScore: number | null; + readonly providerLogo: string; } \ No newline at end of file From 25bbde17035496b97f35ada897525f972487b77e Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Mon, 11 Dec 2023 13:14:05 +0530 Subject: [PATCH 67/88] image Link fix --- prisma/schema.prisma | 2 +- prisma/seed.ts | 16 ++++++++-------- src/admin/admin.service.ts | 2 +- src/admin/dto/provider-settlement.dto.ts | 2 +- src/course/course.controller.ts | 3 ++- src/course/course.service.ts | 22 +++++++++++++--------- src/course/dto/course-response.dto.ts | 2 +- src/course/dto/edit-course.dto.ts | 2 +- src/course/dto/purchase.dto.ts | 2 +- src/provider/provider.service.ts | 12 ++++++------ tsconfig.json | 3 ++- 11 files changed, 37 insertions(+), 31 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0c6932d..c12527c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -71,7 +71,7 @@ model Course { title String description String courseLink String - imgLink String + imageLink String credits Int language String[] competency Json diff --git a/prisma/seed.ts b/prisma/seed.ts index a66a91d..087c8a5 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -84,7 +84,7 @@ async function main() { title: "NestJS Complete", description: "Build full featured backend APIs incredibly quickly with Nest, TypeORM, and Typescript. Includes testing and deployment!", courseLink: "https://www.udemy.com/course/nestjs-the-complete-developers-guide/", - imgLink: "https://courses.nestjs.com/img/logo.svg", + imageLink: "https://courses.nestjs.com/img/logo.svg", credits: 4, language: ["en"], competency: { @@ -102,7 +102,7 @@ async function main() { title: "Graphic Design Masterclass", description: "The Ultimate Graphic Design Course Which Covers Photoshop, Illustrator, InDesign, Design Theory, Branding & Logo Design", courseLink: "https://www.udemy.com/course/graphic-design-masterclass-everything-you-need-to-know/", - imgLink: "https://www.unite.ai/wp-content/uploads/2023/05/emily-bernal-v9vII5gV8Lw-unsplash.jpg", + imageLink: "https://www.unite.ai/wp-content/uploads/2023/05/emily-bernal-v9vII5gV8Lw-unsplash.jpg", credits: 5, language: ["en"], competency: { @@ -119,7 +119,7 @@ async function main() { title: "Python for Data Science", description: "Learn how to use NumPy, Pandas, Seaborn , Matplotlib , Plotly , Scikit-Learn , Machine Learning, Tensorflow , and more", courseLink: "https://www.udemy.com/course/python-for-data-science-and-machine-learning-bootcamp/", - imgLink: "https://blog.imarticus.org/wp-content/uploads/2021/12/learn-Python-for-data-science.jpg", + imageLink: "https://blog.imarticus.org/wp-content/uploads/2021/12/learn-Python-for-data-science.jpg", credits: 2, language: ["en"], competency: { @@ -134,7 +134,7 @@ async function main() { title: "Microsoft Excel", description: "Excel with this A-Z Microsoft Excel Course. Microsoft Excel 2010, 2013, 2016, Excel 2019 and Microsoft/Office 365/2023", courseLink: "https://www.udemy.com/course/microsoft-excel-2013-from-beginner-to-advanced-and-beyond/", - imgLink: "https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Microsoft_Excel_2013-2019_logo.svg/587px-Microsoft_Excel_2013-2019_logo.svg.png", + imageLink: "https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Microsoft_Excel_2013-2019_logo.svg/587px-Microsoft_Excel_2013-2019_logo.svg.png", credits: 4, language: ["en"], competency: { @@ -148,7 +148,7 @@ async function main() { title: "Learn DevOps & Kubernetes", description: "This course enables anyone to get started with devops engineering.", courseLink: "https://udemy.com/courses/pYUxbhj", - imgLink: "https://udemy.com/courses/pYUxbhj/images/cover1.jpg", + imageLink: "https://udemy.com/courses/pYUxbhj/images/cover1.jpg", credits: 120, language: ["english", "hindi"], competency: { @@ -168,7 +168,7 @@ async function main() { title: "Introduction to Programming", description: "This course covers all the fundamentals of programming", courseLink: "https://udemy.com/courses/jQKsLpm", - imgLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", + imageLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, language: ["english", "hindi"], avgRating: 3.5, @@ -184,7 +184,7 @@ async function main() { title: "Introduction to Compiler Engineering", description: "This course covers how compilers are built and also teaches you about how to create custom programming languages", courseLink: "https://udemy.com/courses/jQKsLpm", - imgLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", + imageLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, language: ["english", "hindi"], competency: { @@ -202,7 +202,7 @@ async function main() { title: "Introduction to Compiler Engineering 2", description: "This course covers how compilers are built and also teaches you about how to create custom programming languages", courseLink: "https://udemy.com/courses/jQKsLpm", - imgLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", + imageLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, language: ["english", "hindi"], competency: { diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index fa7e5c8..3ef0e2f 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -30,7 +30,7 @@ export class AdminService { let imageUrl: string | undefined; if(image) { - const imageName = signupDto.name.replace(" ", "_") + const imageName = signupDto.name.replaceAll(" ", "_") imageUrl = await uploadFile(`admin/${imageName}`, image.buffer) } diff --git a/src/admin/dto/provider-settlement.dto.ts b/src/admin/dto/provider-settlement.dto.ts index 7d587ed..a44431e 100644 --- a/src/admin/dto/provider-settlement.dto.ts +++ b/src/admin/dto/provider-settlement.dto.ts @@ -10,7 +10,7 @@ export class ProviderSettlementDto { @ApiProperty() @IsOptional() @IsUrl() - imgLink?: string; + imageLink?: string; @ApiProperty() @IsOptional() diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index 1da501d..b2bcf1c 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -87,12 +87,13 @@ export class CourseController { try { this.logger.log(`Recording the user purchase of the course`); - await this.courseService.addPurchaseRecord(courseId, consumerId); + const response = await this.courseService.addPurchaseRecord(courseId, consumerId); this.logger.log(`Successfully recorded the purchase`); res.status(HttpStatus.OK).json({ message: "purchase successful", + data: response }) } catch (err) { this.logger.error(`Failed to record the purchase`); diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 3b1b988..add2032 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -12,6 +12,7 @@ import { CourseStatusDto } from "./dto/course-status.dto"; import axios from "axios"; import { uploadFile } from "src/utils/minio"; import { ProviderProfileResponse } from "src/provider/dto/provider-profile-response.dto"; +import { PurchaseResponseDto } from "./dto/purchase.dto"; @Injectable() export class CourseService { @@ -86,7 +87,7 @@ export class CourseService { // price: course.credits.toString(), // languages: course.language, // competency: course.competency, - // imgUrl: course.imgLink, + // imgUrl: course.imageLink, // rating: course.avgRating?.toString() || "0", // startTime: new Date().toISOString(), // need to change // endTime: new Date().toISOString(), // need to change @@ -97,8 +98,8 @@ export class CourseService { async addCourse(addCourseDto: AddCourseDto, provider: ProviderProfileResponse, image: Express.Multer.File) { - const imageName = addCourseDto.title.replace(" ", "_") - const imgLink = await uploadFile( `provider/${provider.orgName.replace(" ", "_")}/${imageName}`, image.buffer); + const imageName = addCourseDto.title.replaceAll(" ", "_") + const imageLink = await uploadFile( `provider/${provider.orgName.replaceAll(" ", "_")}/${imageName}`, image.buffer); // add new course to the platform @@ -106,12 +107,12 @@ export class CourseService { data: { providerId: provider.id, ...addCourseDto, - imgLink, + imageLink, } }); } - async addPurchaseRecord(courseId: string, consumerId: string) { + async addPurchaseRecord(courseId: string, consumerId: string): Promise { // Validate course const course = await this.getCourse(courseId); @@ -138,6 +139,9 @@ export class CourseService { courseId, } }); + return { + courseLink: course.courseLink + } } async changeStatus(courseId: string, providerId: string, courseStatusDto: CourseStatusDto) { @@ -159,10 +163,10 @@ export class CourseService { // Validate course const course = await this.getCourse(courseId); - let imgUrl = course.imgLink; + let imgUrl = course.imageLink; if(image) { - const imageName = (editCourseDto.title ?? course.title).replace(" ", "_") - imgUrl = await uploadFile( `provider/${provider.orgName.replace(" ", "_")}/${imageName}`, image.buffer); + const imageName = (editCourseDto.title ?? course.title).replaceAll(" ", "_") + imgUrl = await uploadFile( `provider/${provider.orgName.replaceAll(" ", "_")}/${imageName}`, image.buffer); } // update the course details as required and change its verification status to pending @@ -171,7 +175,7 @@ export class CourseService { data: { ...editCourseDto, verificationStatus: CourseVerificationStatus.PENDING, - imgLink: imgUrl + imageLink: imgUrl } }); } diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index 6b081c2..106e873 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -8,7 +8,7 @@ export class CourseResponse { readonly title: string; readonly description: string; readonly courseLink: string; - readonly imgLink: string; + readonly imageLink: string; readonly credits: number; readonly language: string[]; readonly competency: JsonValue; diff --git a/src/course/dto/edit-course.dto.ts b/src/course/dto/edit-course.dto.ts index 754af5e..e66a535 100644 --- a/src/course/dto/edit-course.dto.ts +++ b/src/course/dto/edit-course.dto.ts @@ -26,7 +26,7 @@ export class EditCourseDto { @ApiProperty() @IsUrl() @IsOptional() - imgLink?: string; + imageLink?: string; // number of credits required to purchase course @ApiProperty() diff --git a/src/course/dto/purchase.dto.ts b/src/course/dto/purchase.dto.ts index 39cb67d..316cb7c 100644 --- a/src/course/dto/purchase.dto.ts +++ b/src/course/dto/purchase.dto.ts @@ -11,7 +11,7 @@ export class PurchaseDto { } export class PurchaseResponseDto { - readonly walletTransactionId: number; + readonly courseLink: string; } export class WalletPurchaseDto { diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index a24a1b8..3e971b3 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -48,7 +48,7 @@ export class ProviderService { throw new BadRequestException("Logo not uploaded"); // upload the image to minio - const imgLink = await uploadFile(`provider/${signupDto.orgName.replace(" ", "_")}/logo`, logo.buffer) + const imageLink = await uploadFile(`provider/${signupDto.orgName.replaceAll(" ", "_")}/logo`, logo.buffer) // Create an entry in the database provider = await this.prisma.provider.create({ @@ -58,7 +58,7 @@ export class ProviderService { password: hashedPassword, paymentInfo: signupDto.paymentInfo ? JSON.parse(signupDto.paymentInfo) : undefined, orgName: signupDto.orgName, - orgLogo: imgLink, + orgLogo: imageLink, phone: signupDto.phone } }); @@ -139,11 +139,11 @@ export class ProviderService { // Fetch provider const provider = await this.getProvider(providerId); - let imgLink = provider.orgLogo; + let imageLink = provider.orgLogo; if(logo) { // upload the image to minio const imageName = updateProfileDto.orgName ?? provider.orgName; - imgLink = await uploadFile(`provider/${imageName.replace(" ", "_")}/logo`, logo.buffer) + imageLink = await uploadFile(`provider/${imageName.replaceAll(" ", "_")}/logo`, logo.buffer) } const { paymentInfo, ...clone } = updateProfileDto; await this.prisma.provider.update({ @@ -153,7 +153,7 @@ export class ProviderService { data: { ...clone, paymentInfo: paymentInfo ? JSON.parse(paymentInfo) : undefined, - orgLogo: imgLink + orgLogo: imageLink } }) } @@ -318,7 +318,7 @@ export class ProviderService { return { id: providerId, name: provider.orgName, - imgLink: provider.orgLogo, + imageLink: provider.orgLogo, totalCourses: courses.length, activeUsers: activeUsers.size } diff --git a/tsconfig.json b/tsconfig.json index 8b9d137..7674d93 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ "strictBindCallApply": false, "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, - "esModuleInterop": true + "esModuleInterop": true, + "lib": ["ES2021.String"] } } From 9c350e0b28c95f52e0ecb22f473d4592883f1c19 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Mon, 11 Dec 2023 16:39:22 +0530 Subject: [PATCH 68/88] seeed change --- prisma/seed.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 087c8a5..b3a3076 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -241,35 +241,35 @@ async function main() { // console.log("All courses: ", resp); const response3 = await prisma.userCourse.createMany({ data: [{ - userId: "123e4567-e89b-42d3-a456-556642440000", + userId: "4d45a9e9-4a4d-4c92-aaea-7b5abbd6ff98", feedback: "Great course", rating: 4, status: CourseProgressStatus.COMPLETED, courseCompletionScore: 100, courseId: "123e4567-e89b-42d3-a456-556642440050" }, { - userId: "123e4567-e89b-42d3-a456-556642440000", + userId: "4d45a9e9-4a4d-4c92-aaea-7b5abbd6ff98", courseId: "123e4567-e89b-42d3-a456-556642440051", status: CourseProgressStatus.COMPLETED, }, { - userId: "123e4567-e89b-42d3-a456-556642440001", + userId: "abaa7220-5d2e-4e05-842a-95b2c4ce1876", feedback: "Instructor is very friendly", rating: 4, status: CourseProgressStatus.COMPLETED, courseCompletionScore: 100, courseId: "123e4567-e89b-42d3-a456-556642440050" }, { - userId: "123e4567-e89b-42d3-a456-556642440001", + userId: "abaa7220-5d2e-4e05-842a-95b2c4ce1876", courseId: "123e4567-e89b-42d3-a456-556642440051" }, { - userId: "123e4567-e89b-42d3-a456-556642440001", + userId: "abaa7220-5d2e-4e05-842a-95b2c4ce1876", feedback: "Some more real world applications could be discussed", rating: 3, status: CourseProgressStatus.COMPLETED, courseCompletionScore: 100, courseId: "123e4567-e89b-42d3-a456-556642440052" }, { - userId: "123e4567-e89b-42d3-a456-556642440002", + userId: "0f5d0b13-8d72-46c9-a7c4-c1f7e5aa1f17", feedback: "Not satisfied with the content", rating: 2, status: CourseProgressStatus.COMPLETED, From 2cc94115cfe89bb8fe1a544f2f28c7f12daa2c19 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Wed, 13 Dec 2023 14:35:54 +0530 Subject: [PATCH 69/88] most popular courses --- README.md | 29 ++++++++++++++- src/course/course.controller.ts | 33 +++++++++++++++++- src/course/course.service.ts | 62 ++++++++++++++++++++++++--------- 3 files changed, 105 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 6aef540..8b1fc07 100644 --- a/README.md +++ b/README.md @@ -1 +1,28 @@ -# Microservice boilerplate +# Course Manager + +[Compass Product Flow](https://miro.com/app/board/uXjVMkv3bh4=/?share_link_id=179469421530) +[Compass Services Diagram](https://app.diagrams.net/#G1ZcWAg558z88DcWNC4b2NKt1Q3MAPHSZu) + +## About +This repository is a part of the Compass Marketplace where consumers and purchase courses and upskill their competencies and third party course providers and onboard and add their courses. It handles the backend server dealing with the use cases of the course providers and partially the admin. Particularly, the entire provider flow on the marketplace which would include adding and updating courses and admin use cases of verifying the providers and courses and settling provider wallet balances. +The tech stack used is NestJS with Prisma ORM and PostgreSQL. + +The Course managermodule is dependent on the modules Marketplace portal and Marketplace Wallet Service. + +## Installation +1. Install the necessary package dependencies + `npm i` +2. Set up a PostgreSQL in your local environment +3. Set up the environment variables as suggested in the example file +4. Generate Prisma migrations + `npx prisma migrate dev` + If seed data is required, it can be populated by running + `npx prisma db seed` + or + `npx prisma migrate reset` + (The latter will also reset the database and delete all previous data) + +## Running A Local Development Server +An auto compiled running server can then be initialized using, + `npm run start:dev` +The Swagger API documentation could be found at `YOUR_APP_PORT/api/docs` \ No newline at end of file diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index b2bcf1c..49626b4 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Get, HttpStatus, Logger, Param, ParseUUIDPipe, Patch, Post, Query, Res } from "@nestjs/common"; +import { Body, Controller, Get, HttpStatus, Logger, Param, ParseIntPipe, ParseUUIDPipe, Patch, Post, Query, Res } from "@nestjs/common"; import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger"; import { CourseService } from "./course.service"; import { FeedbackDto } from "./dto/feedback.dto"; @@ -45,6 +45,37 @@ export class CourseController { } } + @ApiOperation({ summary: 'Get most popular courses' }) + @ApiResponse({ status: HttpStatus.OK, type: [CourseResponse] }) + @Get("/popular") + // Get a list of the most popular courses + async mostPopularCourses( + @Query('limit') limit: number, + @Query('offset') offset: number, + @Res() res + ) { + try { + this.logger.log(`Fetching most popular courses`); + + const courses = await this.courseService.mostPopularCourses(limit, offset); + + this.logger.log(`Successfully fetched the most popular courses`); + + res.status(HttpStatus.OK).json({ + message: "fetch successful", + data: courses + }) + } catch (err) { + this.logger.error(`Failed to fetch courses`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch courses", + }); + } + } + @ApiOperation({ summary: 'Fetch details of one course' }) @ApiResponse({ status: HttpStatus.OK, type: CourseResponse }) @Get("/:courseId") diff --git a/src/course/course.service.ts b/src/course/course.service.ts index add2032..327ce7c 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -77,23 +77,6 @@ export class CourseService { } return courseResponse; }); - // return courses.map((course) => { - // return { - // id: course.id.toString(), - // title: course.title, - // long_desc: course.description, - // provider_id: course.providerId, - // provider_name: course.provider.orgName, - // price: course.credits.toString(), - // languages: course.language, - // competency: course.competency, - // imgUrl: course.imageLink, - // rating: course.avgRating?.toString() || "0", - // startTime: new Date().toISOString(), // need to change - // endTime: new Date().toISOString(), // need to change - // noOfPurchases: course._count.userCourses, - // } - // }); } async addCourse(addCourseDto: AddCourseDto, provider: ProviderProfileResponse, image: Express.Multer.File) { @@ -489,6 +472,51 @@ export class CourseService { }); } + async mostPopularCourses(limit?: number, offset?: number): Promise { + + let courses = await this.prisma.course.findMany({ + where: { + verificationStatus: CourseVerificationStatus.ACCEPTED, + status: CourseStatus.UNARCHIVED, + }, + include: { + provider: { + select: { + orgName: true, + } + }, + _count: { + select: { + userCourses: true + } + } + }, + orderBy: { + avgRating: { + sort: "desc", + nulls: "last" + } + }, + skip: offset ? offset : 0, + take: limit ? limit : 10 + }); + + // Filter out the courses that are not available + courses = courses.filter((c) => (c.startDate ? c.startDate <= new Date(): true) + && (c.endDate ? c.endDate >= new Date(): true) + ); + return courses.map((c) => { + let {cqfScore, impactScore, verificationStatus, rejectionReason, provider, _count, competency, ...clone} = c; + const courseResponse: CourseResponse = { + ...clone, + providerName: provider.orgName, + numOfUsers: _count.userCourses, + competency: (typeof competency == "string") ? JSON.parse(competency) : competency, + } + return courseResponse; + }); + } + async filterVerified(courses: SearchResponseDTO[]) { return courses.filter(async (course) => { From 6deeea4d16e36804d1780cd2124d747be7335f8b Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Wed, 13 Dec 2023 21:57:36 +0530 Subject: [PATCH 70/88] Competency search fix --- src/course/course.service.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 327ce7c..dd4404e 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -42,10 +42,17 @@ export class CourseService { mode: "insensitive", } }, { - competency: { - string_contains: searchInput + competency: { + path: [searchInput.toLowerCase()], + not: "null", } - }] + }, { + competency: { + path: [searchInput], + not: "null", + } + } + ] }, include: { provider: { From 5fb5c0c0b1aee6cb111916ec4db83d0f74c1ee40 Mon Sep 17 00:00:00 2001 From: Aman Agarwal Date: Thu, 14 Dec 2023 01:25:17 +0530 Subject: [PATCH 71/88] Dockerfile completed --- Dockerfile | 29 +++++---------------------- docker-compose.yaml | 48 ++++++++++++++++----------------------------- env-example | 14 ++++++------- package.json | 2 +- 4 files changed, 30 insertions(+), 63 deletions(-) diff --git a/Dockerfile b/Dockerfile index c3a4084..501482c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,22 +1,3 @@ -# RUN apk add --no-cache bash -# RUN npm i -g @nestjs/cli typescript ts-node - -# COPY package*.json /tmp/app/ -# RUN cd /tmp/app && npm install - -# COPY . /usr/src/app -# RUN cp -a /tmp/app/node_modules /usr/src/app -# COPY ./wait-for-it.sh /opt/wait-for-it.sh -# COPY ./startup.dev.sh /opt/startup.dev.sh -# RUN sed -i 's/\r//g' /opt/wait-for-it.sh -# RUN sed -i 's/\r//g' /opt/startup.dev.sh - -# WORKDIR /usr/src/app -# RUN cp env-example .env - -# RUN npm run build - -# CMD ["/opt/startup.dev.sh"] # Use the official Node.js 18 image as a base FROM node:18.16.1-alpine @@ -36,20 +17,20 @@ 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/ - # Install project dependencies RUN npm install # Copy the rest of the application code to the container COPY . . +COPY env-example ./.env +# Expose the PORT environment variable (default to 4000 if not provided) +ENV PORT=4030 +EXPOSE $PORT # Build your Nest.js application RUN npm run build -# Expose the PORT environment variable (default to 4000 if not provided) -ARG PORT=4000 -ENV PORT=$PORT -EXPOSE $PORT + # Start the Nest.js application using the start:prod script CMD ["npm", "run", "start:prod"] diff --git a/docker-compose.yaml b/docker-compose.yaml index 56df483..a82abcc 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,38 +1,24 @@ services: - postgres: - image: postgres:15.3-alpine - ports: - - ${DATABASE_PORT}:5432 - volumes: - - ./.data/db:/var/lib/postgresql/data - environment: - POSTGRES_USER: ${DATABASE_USERNAME} - POSTGRES_PASSWORD: ${DATABASE_PASSWORD} - POSTGRES_DB: ${DATABASE_NAME} - - # maildev: - # build: - # context: . - # dockerfile: maildev.Dockerfile - # ports: - # - ${MAIL_CLIENT_PORT}:1080 - # - ${MAIL_PORT}:1025 - - # adminer: - # image: adminer - # restart: always - # ports: - # - 8080:8080 +# postgres: +# image: postgres:15.3-alpine +# ports: +# - ${DATABASE_PORT}:5432 +# volumes: +# - ./.data/db:/var/lib/postgresql/data +# environment: +# POSTGRES_USER: ${DATABASE_USERNAME} +# POSTGRES_PASSWORD: ${DATABASE_PASSWORD} +# POSTGRES_DB: ${DATABASE_NAME} - # Uncomment to use redis - # redis: - # image: redis:7-alpine - # ports: - # - 6379:6379 - - api: + course_manager: build: context: . dockerfile: Dockerfile ports: - ${APP_PORT}:${APP_PORT} + networks: + - samagra_compass + +networks: + samagra_compass: + external: true \ No newline at end of file diff --git a/env-example b/env-example index eddeb12..f914219 100644 --- a/env-example +++ b/env-example @@ -1,11 +1,11 @@ NODE_ENV=development -APP_PORT=4000 -APP_NAME="Service API" +APP_PORT=4030 +APP_NAME="Course Manager API" API_PREFIX=api -DATABASE_USERNAME= -DATABASE_PASSWORD= -DATABASE_NAME= +DATABASE_USERNAME=postgres +DATABASE_PASSWORD=secret +DATABASE_NAME=course-manager-db DATABASE_PORT=5432 -DATABASE_URL= -WALLET_SERVICE_URL= \ No newline at end of file +DATABASE_URL="postgresql://postgres:secret@postgres:5432/course-manager-db?sslmode=prefer" +WALLET_SERVICE_URL=http://0.0.0.0:4022 \ No newline at end of file diff --git a/package.json b/package.json index 53a6ee0..5a7c7ce 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,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/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "test": "jest", "test:watch": "jest --watch", From d9e5907e2b138a77a0a644b8f7140eabc05eaa18 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Thu, 14 Dec 2023 11:11:22 +0530 Subject: [PATCH 72/88] recommended courses --- prisma/seed.ts | 37 ++++++++++++------------ src/course/course.controller.ts | 30 +++++++++++++++++++ src/course/course.service.ts | 51 +++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 18 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index b3a3076..8f833e3 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -87,11 +87,11 @@ async function main() { imageLink: "https://courses.nestjs.com/img/logo.svg", credits: 4, language: ["en"], - competency: { - "API Development": ["Level1", "Level2"], + competency: JSON.stringify({ + "NestJs": ["Level1", "Level2"], "Typescript": ["Level1"], "Backend engineering": ["Level1"] - }, + }), author: "Stephen Grider", startDate: new Date("2023-05-01").toISOString(), endDate: new Date("2024-07-01").toISOString(), @@ -105,10 +105,10 @@ async function main() { imageLink: "https://www.unite.ai/wp-content/uploads/2023/05/emily-bernal-v9vII5gV8Lw-unsplash.jpg", credits: 5, language: ["en"], - competency: { + competency: JSON.stringify({ "Photoshop": ["Level2", "Level3"], "Understanding brand": ["Level1"] - }, + }), author: "Lindsay Marsh", startDate: new Date("2023-05-01").toISOString(), endDate: new Date("2024-09-01").toISOString(), @@ -122,11 +122,11 @@ async function main() { imageLink: "https://blog.imarticus.org/wp-content/uploads/2021/12/learn-Python-for-data-science.jpg", credits: 2, language: ["en"], - competency: { + competency: JSON.stringify({ "Statistics": ["Level1"], "Machine Learning": ["Level1", "Level2", "Level3"], "MySQL": ["Level1"] - }, + }), author: "Jose Portilla", }, { courseId: "123e4567-e89b-42d3-a456-556642440053", @@ -137,9 +137,9 @@ async function main() { imageLink: "https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Microsoft_Excel_2013-2019_logo.svg/587px-Microsoft_Excel_2013-2019_logo.svg.png", credits: 4, language: ["en"], - competency: { + competency: JSON.stringify({ "Excel": ["Level1", "Level2", "Level3", "Level4"] - }, + }), author: "Kyle Pew", startDate: new Date("2024-05-01").toISOString(), }, { @@ -151,11 +151,12 @@ async function main() { imageLink: "https://udemy.com/courses/pYUxbhj/images/cover1.jpg", credits: 120, language: ["english", "hindi"], - competency: { + competency: JSON.stringify({ "Docker": ["Level1", "Level3"], "Kubernetes": ["Level1"], - "Orchestration": [ "Level5" ] - }, + "Orchestration": [ "Level5" ], + "Micro Architecture": [ "Level1" ] + }), author: "Jason Frig", startDate: new Date("2023-06-01"), endDate: new Date("2024-08-01"), @@ -172,10 +173,10 @@ async function main() { credits: 160, language: ["english", "hindi"], avgRating: 3.5, - competency: { + competency: JSON.stringify({ "Logical Thinking": ["Level5", "Level4"], "Python": [ "Level1", "Level2" ] - }, + }), author: "James Franco", verificationStatus: CourseVerificationStatus.PENDING, }, { @@ -187,10 +188,10 @@ async function main() { imageLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, language: ["english", "hindi"], - competency: { + competency: JSON.stringify({ "Compiler Design": ["Level2", "Level3"], "LLVM": [ "Level4" ] - }, + }), author: "Ramakrishna Upadrasta", startDate: new Date("2023-10-10"), endDate: new Date("2023-11-10"), @@ -205,10 +206,10 @@ async function main() { imageLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, language: ["english", "hindi"], - competency: { + competency: JSON.stringify({ "Compiler Design": ["Level2", "Level3"], "LLVM": [ "Level4" ] - }, + }), author: "Ramakrishna Upadrasta", startDate: new Date("2023-10-10"), endDate: new Date("2023-11-10"), diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index 49626b4..db2a5f4 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -76,6 +76,36 @@ export class CourseController { } } + @ApiOperation({ summary: 'Get recommended courses' }) + @ApiResponse({ status: HttpStatus.OK, type: [CourseResponse] }) + @Get("/recommended") + // Get a list of the recommended courses + async recommendedCourses( + @Query('competencies') competencies: string[], + @Res() res + ) { + try { + this.logger.log(`Fetching recommended courses`); + + const courses = await this.courseService.recommendedCourses(competencies); + + this.logger.log(`Successfully fetched the recommended courses`); + + res.status(HttpStatus.OK).json({ + message: "fetch successful", + data: courses + }) + } catch (err) { + this.logger.error(`Failed to fetch courses`); + + const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); + res.status(statusCode).json({ + statusCode, + message: errorMessage || "Failed to fetch courses", + }); + } + } + @ApiOperation({ summary: 'Fetch details of one course' }) @ApiResponse({ status: HttpStatus.OK, type: CourseResponse }) @Get("/:courseId") diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 327ce7c..282f789 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -472,6 +472,57 @@ export class CourseService { }); } + async recommendedCourses(competencies: string[]): Promise { + + if(typeof competencies == "string") + competencies = [competencies]; + + let competencyFilter = (competencies) ? competencies.map(( competency ) => { + + return { + competency: { + string_contains: competency + } + } + }): undefined; + let courses = await this.prisma.course.findMany({ + where: { + verificationStatus: CourseVerificationStatus.ACCEPTED, + status: CourseStatus.UNARCHIVED, + OR: competencyFilter + }, + include: { + provider: { + select: { + orgName: true, + } + }, + _count: { + select: { + userCourses: true + } + } + }, + }); + + // Filter out the courses that are not available + courses = courses.filter((c) => (c.startDate ? c.startDate <= new Date(): true) + && (c.endDate ? c.endDate >= new Date(): true) + ); + return courses.map((c) => { + let {cqfScore, impactScore, verificationStatus, rejectionReason, provider, _count, competency, ...clone} = c; + + const courseResponse: CourseResponse = { + ...clone, + providerName: provider.orgName, + numOfUsers: _count.userCourses, + competency: (typeof competency == "string") ? JSON.parse(competency) : competency, + } + return courseResponse; + }); + } + + async mostPopularCourses(limit?: number, offset?: number): Promise { let courses = await this.prisma.course.findMany({ From 1204a0bcd5fb56de8b87aa378a3739d60b5c5aa8 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Thu, 14 Dec 2023 13:19:06 +0530 Subject: [PATCH 73/88] courseLink fix --- src/course/course.service.ts | 8 ++++---- src/course/dto/course-response.dto.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/course/course.service.ts b/src/course/course.service.ts index e96b030..b4063a1 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -75,7 +75,7 @@ export class CourseService { && (c.endDate ? c.endDate >= new Date(): true) ); return courses.map((c) => { - let {cqfScore, impactScore, verificationStatus, rejectionReason, provider, _count, competency, ...clone} = c; + let {cqfScore, impactScore, verificationStatus, rejectionReason, provider, _count, competency, courseLink, ...clone} = c; const courseResponse: CourseResponse = { ...clone, providerName: provider.orgName, @@ -221,7 +221,7 @@ export class CourseService { if((course.startDate && course.startDate > new Date()) || (course.endDate && course.endDate < new Date())) throw new BadRequestException("Course is not available at the moment"); - const {cqfScore, impactScore, verificationStatus, rejectionReason, ...clone} = course; + const {cqfScore, impactScore, verificationStatus, rejectionReason, courseLink, ...clone} = course; return { ...clone, numOfUsers @@ -517,7 +517,7 @@ export class CourseService { && (c.endDate ? c.endDate >= new Date(): true) ); return courses.map((c) => { - let {cqfScore, impactScore, verificationStatus, rejectionReason, provider, _count, competency, ...clone} = c; + let {cqfScore, impactScore, verificationStatus, rejectionReason, courseLink, provider, _count, competency, ...clone} = c; const courseResponse: CourseResponse = { ...clone, @@ -564,7 +564,7 @@ export class CourseService { && (c.endDate ? c.endDate >= new Date(): true) ); return courses.map((c) => { - let {cqfScore, impactScore, verificationStatus, rejectionReason, provider, _count, competency, ...clone} = c; + let {cqfScore, impactScore, verificationStatus, rejectionReason, courseLink, provider, _count, competency, ...clone} = c; const courseResponse: CourseResponse = { ...clone, providerName: provider.orgName, diff --git a/src/course/dto/course-response.dto.ts b/src/course/dto/course-response.dto.ts index 106e873..ecfe1f9 100644 --- a/src/course/dto/course-response.dto.ts +++ b/src/course/dto/course-response.dto.ts @@ -7,7 +7,6 @@ export class CourseResponse { readonly providerId: string; readonly title: string; readonly description: string; - readonly courseLink: string; readonly imageLink: string; readonly credits: number; readonly language: string[]; @@ -24,6 +23,7 @@ export class CourseResponse { } export class ProviderCourseResponse extends CourseResponse { + readonly courseLink: string; readonly verificationStatus: CourseVerificationStatus; readonly rejectionReason: string | null; } From daf006036acc25b0459dfd71ba8865861f4af0ee Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 15 Dec 2023 13:05:42 +0530 Subject: [PATCH 74/88] remove old image --- src/utils/minio.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/minio.ts b/src/utils/minio.ts index e10752e..219beaa 100644 --- a/src/utils/minio.ts +++ b/src/utils/minio.ts @@ -22,6 +22,9 @@ export async function uploadFile(objectName: string, fileBuffer: Buffer) { if (!exists) { throw new Error("Bucket not found") } + + // Remove the file if it already exists + await minioClient.removeObject(bucketName, objectName); // Upload the file to the specified bucket and object await minioClient.putObject(bucketName, objectName, fileBuffer); From ca01761fd17ce23fc2b58290e926e6ab0e24f213 Mon Sep 17 00:00:00 2001 From: Aman Agarwal Date: Fri, 15 Dec 2023 13:53:30 +0530 Subject: [PATCH 75/88] updating env file --- env-example | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/env-example b/env-example index 95ce568..a505408 100644 --- a/env-example +++ b/env-example @@ -8,11 +8,4 @@ DATABASE_PASSWORD=secret DATABASE_NAME=course-manager-db DATABASE_PORT=5432 DATABASE_URL="postgresql://postgres:secret@postgres:5432/course-manager-db?sslmode=prefer" -WALLET_SERVICE_URL= -MARKETPLACE_PORTAL_URL= - -# MINIO -MINIO_ACCESS_KEY= -MINIO_SECRET_KEY= -MINIO_ENDPOINT= -MINIO_BUCKET_NAME= +WALLET_SERVICE_URL=http://wallet:4022 \ No newline at end of file From 467e651e7a689a8242a70d84cdc6090b8fb3ba29 Mon Sep 17 00:00:00 2001 From: Aman Agarwal Date: Fri, 15 Dec 2023 14:24:41 +0530 Subject: [PATCH 76/88] adding new migration file --- .gitignore | 3 - env-example | 9 +- .../20231215085146_first/migration.sql | 101 ++++++++++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 prisma/migrations/20231215085146_first/migration.sql diff --git a/.gitignore b/.gitignore index 8910bfb..5fea95f 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,3 @@ lerna-debug.log* !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json - -# prisma -prisma/migrations \ No newline at end of file diff --git a/env-example b/env-example index a505408..6cf620e 100644 --- a/env-example +++ b/env-example @@ -8,4 +8,11 @@ DATABASE_PASSWORD=secret DATABASE_NAME=course-manager-db DATABASE_PORT=5432 DATABASE_URL="postgresql://postgres:secret@postgres:5432/course-manager-db?sslmode=prefer" -WALLET_SERVICE_URL=http://wallet:4022 \ No newline at end of file +WALLET_SERVICE_URL=http://wallet:4022 +MARKETPLACE_PORTAL_URL=https://marketplace.backend.compass.samagra.io + +# MINIO +MINIO_ACCESS_KEY= +MINIO_SECRET_KEY= +MINIO_ENDPOINT= +MINIO_BUCKET_NAME= diff --git a/prisma/migrations/20231215085146_first/migration.sql b/prisma/migrations/20231215085146_first/migration.sql new file mode 100644 index 0000000..3b20530 --- /dev/null +++ b/prisma/migrations/20231215085146_first/migration.sql @@ -0,0 +1,101 @@ +-- CreateEnum +CREATE TYPE "ProviderStatus" AS ENUM ('PENDING', 'VERIFIED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "WalletType" AS ENUM ('ADMIN', 'PROVIDER', 'CONSUMER'); + +-- CreateEnum +CREATE TYPE "CourseVerificationStatus" AS ENUM ('PENDING', 'ACCEPTED', 'REJECTED'); + +-- CreateEnum +CREATE TYPE "CourseStatus" AS ENUM ('UNARCHIVED', 'ARCHIVED'); + +-- CreateEnum +CREATE TYPE "CourseProgressStatus" AS ENUM ('IN_PROGRESS', 'COMPLETED'); + +-- CreateEnum +CREATE TYPE "TransactionType" AS ENUM ('PURCHASE', 'CREDIT_REQUEST', 'SETTLEMENT'); + +-- CreateTable +CREATE TABLE "Admin" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "image" TEXT, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + + CONSTRAINT "Admin_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Provider" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "orgName" TEXT NOT NULL, + "orgLogo" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "paymentInfo" JSONB, + "status" "ProviderStatus" NOT NULL DEFAULT 'PENDING', + "rejectionReason" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Provider_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Course" ( + "courseId" TEXT NOT NULL, + "providerId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "courseLink" TEXT NOT NULL, + "imageLink" TEXT NOT NULL, + "credits" INTEGER NOT NULL, + "language" TEXT[], + "competency" JSONB NOT NULL, + "author" TEXT NOT NULL, + "avgRating" DOUBLE PRECISION, + "status" "CourseStatus" NOT NULL DEFAULT 'UNARCHIVED', + "startDate" TIMESTAMP(3), + "endDate" TIMESTAMP(3), + "verificationStatus" "CourseVerificationStatus" NOT NULL DEFAULT 'PENDING', + "cqfScore" INTEGER, + "impactScore" DOUBLE PRECISION, + "rejectionReason" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Course_pkey" PRIMARY KEY ("courseId") +); + +-- CreateTable +CREATE TABLE "UserCourse" ( + "id" SERIAL NOT NULL, + "userId" UUID NOT NULL, + "courseId" TEXT NOT NULL, + "purchasedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "status" "CourseProgressStatus" NOT NULL DEFAULT 'IN_PROGRESS', + "courseCompletionScore" DOUBLE PRECISION, + "rating" INTEGER, + "feedback" TEXT, + + CONSTRAINT "UserCourse_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Admin_email_key" ON "Admin"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Provider_email_key" ON "Provider"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "UserCourse_userId_courseId_key" ON "UserCourse"("userId", "courseId"); + +-- AddForeignKey +ALTER TABLE "Course" ADD CONSTRAINT "Course_providerId_fkey" FOREIGN KEY ("providerId") REFERENCES "Provider"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserCourse" ADD CONSTRAINT "UserCourse_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("courseId") ON DELETE CASCADE ON UPDATE CASCADE; From eb1ca3995d5ca4d31f102e82856c35b44f058d83 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 15 Dec 2023 17:26:34 +0530 Subject: [PATCH 77/88] seed --- prisma/seed.ts | 302 ++++++++++++++++++++++--- src/course/dto/edit-course.dto.ts | 1 - src/course/dto/search.dto.ts | 16 -- src/provider/dto/update-profile.dto.ts | 3 +- src/utils/minio.ts | 2 +- src/utils/types.ts | 2 - 6 files changed, 269 insertions(+), 57 deletions(-) delete mode 100644 src/course/dto/search.dto.ts diff --git a/prisma/seed.ts b/prisma/seed.ts index 8f833e3..6626356 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -87,11 +87,62 @@ async function main() { imageLink: "https://courses.nestjs.com/img/logo.svg", credits: 4, language: ["en"], - competency: JSON.stringify({ - "NestJs": ["Level1", "Level2"], - "Typescript": ["Level1"], - "Backend engineering": ["Level1"] - }), + competency: JSON.stringify([{ + "id": 1, + "name": "API Development", + "levels": [ + { + "id": 1, + "levelNumber": 1, + "name": "Basic" + }, { + "id": 2, + "levelNumber": 2, + "name": "Intermediate" + }, { + "id": 3, + "levelNumber": 3, + "name": "Advanced" + } + ] + }, { + "id": 2, + "name": "Typescript", + "levels": [ + { + "id": 4, + "levelNumber": 1, + "name": "Basic" + }, { + "id": 5, + "levelNumber": 2, + "name": "Intermediate" + }, { + "id": 6, + "levelNumber": 3, + "name": "Advanced" + } + ] + }, { + "id": 3, + "name": "Backend engineering", + "levels": [ + { + "id": 7, + "levelNumber": 1, + "name": "Basic" + }, { + "id": 8, + "levelNumber": 2, + "name": "Intermediate" + }, { + "id": 9, + "levelNumber": 3, + "name": "Advanced" + } + ] + } + ]), author: "Stephen Grider", startDate: new Date("2023-05-01").toISOString(), endDate: new Date("2024-07-01").toISOString(), @@ -105,10 +156,31 @@ async function main() { imageLink: "https://www.unite.ai/wp-content/uploads/2023/05/emily-bernal-v9vII5gV8Lw-unsplash.jpg", credits: 5, language: ["en"], - competency: JSON.stringify({ - "Photoshop": ["Level2", "Level3"], - "Understanding brand": ["Level1"] - }), + competency: JSON.stringify([{ + "id": 1, + "name": "Photoshop", + "levels": [{ + "id": 2, + "levelNumber": 2, + "name": "Intermediate" + }, { + "id": 3, + "levelNumber": 3, + "name": "Advanced" + } + ] + }, { + "id": 2, + "name": "Understanding brand", + "levels": [ + { + "id": 4, + "levelNumber": 1, + "name": "Basic" + } + ] + } + ]), author: "Lindsay Marsh", startDate: new Date("2023-05-01").toISOString(), endDate: new Date("2024-09-01").toISOString(), @@ -122,11 +194,44 @@ async function main() { imageLink: "https://blog.imarticus.org/wp-content/uploads/2021/12/learn-Python-for-data-science.jpg", credits: 2, language: ["en"], - competency: JSON.stringify({ - "Statistics": ["Level1"], - "Machine Learning": ["Level1", "Level2", "Level3"], - "MySQL": ["Level1"] - }), + competency: JSON.stringify([{ + "id": 1, + "name": "Statistics", + "levels": [ + { + "id": 1, + "levelNumber": 1, + "name": "Basic" + } + ] + }, { + "id": 2, + "name": "Machine Learning", + "levels": [ + { + "id": 4, + "levelNumber": 1, + "name": "Basic" + }, { + "id": 5, + "levelNumber": 2, + "name": "Intermediate" + }, { + "id": 6, + "levelNumber": 3, + "name": "Advanced" + } + ] + }, { + "id": 3, + "name": "MySQL", + "levels": [{ + "id": 7, + "levelNumber": 1, + "name": "Basic" + }] + } + ]), author: "Jose Portilla", }, { courseId: "123e4567-e89b-42d3-a456-556642440053", @@ -137,9 +242,29 @@ async function main() { imageLink: "https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Microsoft_Excel_2013-2019_logo.svg/587px-Microsoft_Excel_2013-2019_logo.svg.png", credits: 4, language: ["en"], - competency: JSON.stringify({ - "Excel": ["Level1", "Level2", "Level3", "Level4"] - }), + competency: JSON.stringify([{ + "id": 1, + "name": "Excel", + "levels": [ + { + "id": 1, + "levelNumber": 1, + "name": "Basic" + }, { + "id": 5, + "levelNumber": 2, + "name": "Intermediate" + }, { + "id": 6, + "levelNumber": 3, + "name": "Advanced" + }, { + "id": 9, + "levelNumber": 4, + "name": "Export" + } + ] + }]), author: "Kyle Pew", startDate: new Date("2024-05-01").toISOString(), }, { @@ -151,12 +276,52 @@ async function main() { imageLink: "https://udemy.com/courses/pYUxbhj/images/cover1.jpg", credits: 120, language: ["english", "hindi"], - competency: JSON.stringify({ - "Docker": ["Level1", "Level3"], - "Kubernetes": ["Level1"], - "Orchestration": [ "Level5" ], - "Micro Architecture": [ "Level1" ] - }), + competency: JSON.stringify([{ + "id": 1, + "name": "Docker", + "levels": [ + { + "id": 1, + "levelNumber": 1, + "name": "Basic" + }, { + "id": 6, + "levelNumber": 3, + "name": "Advanced" + } + ] + }, { + "id": 2, + "name": "Kubernetes", + "levels": [ + { + "id": 4, + "levelNumber": 1, + "name": "Basic" + }, { + "id": 5, + "levelNumber": 2, + "name": "Intermediate" + } + ] + }, { + "id": 3, + "name": "Orchestration", + "levels": [{ + "id": 7, + "levelNumber": 2, + "name": "Intermediate" + }] + }, { + "id": 4, + "name": "Micro Architecture", + "levels": [{ + "id": 7, + "levelNumber": 1, + "name": "Basic" + }] + } + ]), author: "Jason Frig", startDate: new Date("2023-06-01"), endDate: new Date("2024-08-01"), @@ -173,10 +338,35 @@ async function main() { credits: 160, language: ["english", "hindi"], avgRating: 3.5, - competency: JSON.stringify({ - "Logical Thinking": ["Level5", "Level4"], - "Python": [ "Level1", "Level2" ] - }), + competency: JSON.stringify([{ + "id": 1, + "name": "Logical Thinking", + "levels": [ + { + "id": 1, + "levelNumber": 1, + "name": "Basic" + }, { + "id": 6, + "levelNumber": 3, + "name": "Advanced" + } + ] + }, { + "id": 2, + "name": "Python", + "levels": [ + { + "id": 4, + "levelNumber": 1, + "name": "Basic" + }, { + "id": 5, + "levelNumber": 2, + "name": "Intermediate" + } + ] + }]), author: "James Franco", verificationStatus: CourseVerificationStatus.PENDING, }, { @@ -188,10 +378,31 @@ async function main() { imageLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, language: ["english", "hindi"], - competency: JSON.stringify({ - "Compiler Design": ["Level2", "Level3"], - "LLVM": [ "Level4" ] - }), + competency: JSON.stringify([{ + "id": 1, + "name": "Compiler Design", + "levels": [ + { + "id": 1, + "levelNumber": 2, + "name": "Intermediate" + }, { + "id": 6, + "levelNumber": 3, + "name": "Advanced" + } + ] + }, { + "id": 2, + "name": "LLVM", + "levels": [ + { + "id": 4, + "levelNumber": 4, + "name": "Expert" + } + ] + }]), author: "Ramakrishna Upadrasta", startDate: new Date("2023-10-10"), endDate: new Date("2023-11-10"), @@ -206,10 +417,31 @@ async function main() { imageLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, language: ["english", "hindi"], - competency: JSON.stringify({ - "Compiler Design": ["Level2", "Level3"], - "LLVM": [ "Level4" ] - }), + competency: JSON.stringify([{ + "id": 1, + "name": "Compiler Design", + "levels": [ + { + "id": 1, + "levelNumber": 2, + "name": "Intermediate" + }, { + "id": 6, + "levelNumber": 3, + "name": "Advanced" + } + ] + }, { + "id": 2, + "name": "LLVM", + "levels": [ + { + "id": 4, + "levelNumber": 4, + "name": "Expert" + } + ] + }]), author: "Ramakrishna Upadrasta", startDate: new Date("2023-10-10"), endDate: new Date("2023-11-10"), diff --git a/src/course/dto/edit-course.dto.ts b/src/course/dto/edit-course.dto.ts index e66a535..90c82b6 100644 --- a/src/course/dto/edit-course.dto.ts +++ b/src/course/dto/edit-course.dto.ts @@ -1,6 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; import { IsArray, IsDate, IsInt, IsOptional, IsString, IsUrl, Min } from "class-validator"; -import { CompetencyMap } from "src/utils/types"; export class EditCourseDto { diff --git a/src/course/dto/search.dto.ts b/src/course/dto/search.dto.ts deleted file mode 100644 index b71ee39..0000000 --- a/src/course/dto/search.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ApiProperty } from "@nestjs/swagger"; -import { IsNotEmpty, IsString } from "class-validator"; -import { CompetencyMap } from "src/utils/types"; - -export class SearchDto { - - // Search string - @ApiProperty() - @IsNotEmpty() - @IsString() - searchInput: string; - - // competency filters - @ApiProperty() - competencies: CompetencyMap; -} \ No newline at end of file diff --git a/src/provider/dto/update-profile.dto.ts b/src/provider/dto/update-profile.dto.ts index 7a66eed..5f5f28f 100644 --- a/src/provider/dto/update-profile.dto.ts +++ b/src/provider/dto/update-profile.dto.ts @@ -1,6 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; -import { IsEmail, IsJSON, IsObject, IsOptional, IsPhoneNumber, IsString } from "class-validator"; -import { PaymentInfo } from "src/utils/types"; +import { IsEmail, IsOptional, IsPhoneNumber, IsString } from "class-validator"; export class UpdateProfileDto { diff --git a/src/utils/minio.ts b/src/utils/minio.ts index 219beaa..2de49cf 100644 --- a/src/utils/minio.ts +++ b/src/utils/minio.ts @@ -28,7 +28,7 @@ export async function uploadFile(objectName: string, fileBuffer: Buffer) { // Upload the file to the specified bucket and object await minioClient.putObject(bucketName, objectName, fileBuffer); - console.log('File uploaded successfully!'); + // console.log('File uploaded successfully!'); // minioClient.presignedUrl('GET', bucketName, objectName, 24 * 60 * 60, function (err, presignedUrl) { // if (err) return console.log(err) diff --git a/src/utils/types.ts b/src/utils/types.ts index 012b667..0b393f3 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,6 +1,4 @@ -export type CompetencyMap = Record; - export type PaymentInfo = { bankName: string; branch: string; From 7d7604a95df45886941d7dd41f32d2eac621da3d Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Fri, 15 Dec 2023 19:59:19 +0530 Subject: [PATCH 78/88] seed change --- prisma/seed.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 8f833e3..8affbdc 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -6,6 +6,9 @@ async function main() { const saltRounds = 10; const hashedPassword = await bcrypt.hash("123456", saltRounds); + const hashedPassword2 = await bcrypt.hash("Udemy@9812", saltRounds); + const hashedPassword3 = await bcrypt.hash("Coursera@999", saltRounds); + const hashedPassword4 = await bcrypt.hash("lern@999", saltRounds); const response = await prisma.provider.create({ data: { id: "123e4567-e89b-42d3-a456-556642440010", @@ -24,7 +27,7 @@ async function main() { id: "123e4567-e89b-42d3-a456-556642440011", name: "udemy", email: "udemyorg@gmail.in", - password: "Udemy@9812", + password: hashedPassword2, paymentInfo: { bankAccNo: "1111111111", otherDetails: { @@ -43,7 +46,7 @@ async function main() { id: "123e4567-e89b-42d3-a456-556642440012", name: "coursera", email: "coursera@gmail.in", - password: "Coursera@999", + password: hashedPassword3, paymentInfo: { bankAccNo: "1111111113", otherDetails: { @@ -62,7 +65,7 @@ async function main() { id: "123e4567-e89b-42d3-a456-556642440013", name: "lern", email: "lern@gmail.in", - password: "lern@999", + password: hashedPassword4, paymentInfo: { bankAccNo: "1111111116", otherDetails: { @@ -147,8 +150,8 @@ async function main() { providerId: provider1.id, title: "Learn DevOps & Kubernetes", description: "This course enables anyone to get started with devops engineering.", - courseLink: "https://udemy.com/courses/pYUxbhj", - imageLink: "https://udemy.com/courses/pYUxbhj/images/cover1.jpg", + courseLink: "https://www.udemy.com/course/devops-with-docker-kubernetes-and-azure-devops/", + imageLink: "https://img-c.udemycdn.com/course/240x135/5030480_b416_2.jpg", credits: 120, language: ["english", "hindi"], competency: JSON.stringify({ From 23ff6a665501bf23178e820c5e84ea210cb72aa3 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Mon, 18 Dec 2023 18:10:39 +0530 Subject: [PATCH 79/88] course completion fix --- env-example | 1 + src/course/course.service.ts | 50 +++++++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/env-example b/env-example index 6cf620e..6df08e7 100644 --- a/env-example +++ b/env-example @@ -10,6 +10,7 @@ DATABASE_PORT=5432 DATABASE_URL="postgresql://postgres:secret@postgres:5432/course-manager-db?sslmode=prefer" WALLET_SERVICE_URL=http://wallet:4022 MARKETPLACE_PORTAL_URL=https://marketplace.backend.compass.samagra.io +USER_SERVICE_URL= # MINIO MINIO_ACCESS_KEY= diff --git a/src/course/course.service.ts b/src/course/course.service.ts index b4063a1..55128c5 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -327,6 +327,19 @@ export class CourseService { userId: completeCourseDto.userId, courseId: completeCourseDto.courseId } + }, + include: { + course: { + select: { + title: true, + courseLink: true, + provider: { + select: { + orgName: true + } + } + } + } } }); if(!userCourse) @@ -337,11 +350,40 @@ export class CourseService { if(!uri) throw new HttpException("Marketplace URL not set", 500); - const endpoint = `/api/consumer/${completeCourseDto.userId}/course/complete`; - const courseIdDto = { - courseId: completeCourseDto.courseId, + // Fetch user details from user service + let endpoint = `/api/consumer/${completeCourseDto.userId}/course/complete`; + + if(!process.env.USER_SERVICE_URL) + throw new HttpException("User service URL not defined", 500); + + endpoint = `/api/mockFracService/user/${completeCourseDto.userId}`; + + const userResponse = await axios.get(process.env.USER_SERVICE_URL + endpoint); + + const customer = { + name: userResponse.data.data.userName, + phone: userResponse.data.data.phone || "+919999999999", + email: userResponse.data.data.email } - await axios.patch(uri + endpoint, courseIdDto); + + endpoint = `/api/consumer/course/complete`; + const courseCompletionDto = { + messageId: "123e4567-e89b-42d3-a456-556642440000", + transactionId: "123e4567-e89b-42d3-a456-556642440000", + bppId: "compass.bpp.course_manager", + bppUri: "course.backend.compass.samagra.io", + providerId: "123e4567-e89b-42d3-a456-556642440011", + providerName: userCourse.course.provider.orgName, + providerCourseId: userCourse.courseId, + providerOrderId: "123e4567-e89b-42d3-a456-556642440000", + courseName: userCourse.course.title, + courseLink: userCourse.course.courseLink, + customer, + price: {}, + status: "COMPLETED" + } + + await axios.patch(uri + endpoint, courseCompletionDto); // Update a course as complete for a purchased course await this.prisma.userCourse.update({ From 93941928e3548b32913cde45857ed240d84c94e3 Mon Sep 17 00:00:00 2001 From: SanchitEsMagico Date: Mon, 18 Dec 2023 19:10:27 +0530 Subject: [PATCH 80/88] endpoint fix --- src/course/course.service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/course/course.service.ts b/src/course/course.service.ts index 55128c5..6e6c648 100644 --- a/src/course/course.service.ts +++ b/src/course/course.service.ts @@ -351,12 +351,11 @@ export class CourseService { throw new HttpException("Marketplace URL not set", 500); // Fetch user details from user service - let endpoint = `/api/consumer/${completeCourseDto.userId}/course/complete`; if(!process.env.USER_SERVICE_URL) throw new HttpException("User service URL not defined", 500); - endpoint = `/api/mockFracService/user/${completeCourseDto.userId}`; + let endpoint = `/api/mockFracService/user/${completeCourseDto.userId}`; const userResponse = await axios.get(process.env.USER_SERVICE_URL + endpoint); From 1d83bfae26949566444b82f07038d0fec4dc6b7a Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Wed, 3 Apr 2024 20:46:24 +0530 Subject: [PATCH 81/88] Changes to integrate user service --- src/admin/admin.service.ts | 115 +++++++++++++++++++------------ src/provider/provider.service.ts | 96 ++++++++++++++++---------- 2 files changed, 130 insertions(+), 81 deletions(-) diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index 3ef0e2f..c490e71 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -34,63 +34,88 @@ export class AdminService { imageUrl = await uploadFile(`admin/${imageName}`, image.buffer) } - // Hashing the password - const hashedPassword = await this.authService.hashPassword(signupDto.password); - - const admin = await this.prisma.admin.create({ - data: { - name: signupDto.name, - email: signupDto.email, - password: hashedPassword, - image: imageUrl - } - }); - try { - // Forward to wallet service for creation of wallet - if(!process.env.WALLET_SERVICE_URL) - throw new HttpException("Wallet service URL not defined", 500); + // check if there is a user with admin role in the user service + const baseUrl = "https://compass-dev.tarento.com/api/user/v4/user/search"; + const headers = { + 'Authorization': 'bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzRjB4UmVWNTJLcTc3R0xycnlKT2N2cXQwNVpUZTYySyJ9.ia3j2Pr-IFuioVXzerZjSNwC3HKvj-YcwjvkxOUNx0o', + 'Content-Type': 'application/json', + 'Cookie': 'connect.sid=s%3AXThrvZBYAza6cyO1AN6v2_6KOL5EM5cI.tQhpI4ryxMsIAvhAW6A%2Fkc9pAr5sxoC41PnTwUemWP0' + }; - const url = process.env.WALLET_SERVICE_URL; - const endpoint = url + `/api/wallet/create`; - const reqBody = { - userId: admin.id, - type: 'ADMIN', - credits: 0 + const data = { + "request": { + "filters": { + "profileDetails.personalDetails.primaryEmail": signupDto.email + } } - await axios.post(endpoint, reqBody); - } catch(err) { - await this.prisma.admin.delete({ - where: { - id: admin.id + }; + + let response = await axios.post(baseUrl, data, {headers}); + console.log("Response from user service: ", JSON.stringify(response.data.result?.response?.content[0]?.organisations[0])); + if(response.data.result?.response?.content[0]?.organisations[0]?.roles?.filter(role => role == "ADMIN").length != 0 ) { + // Hashing the password + const hashedPassword = await this.authService.hashPassword(signupDto.password); + + const admin = await this.prisma.admin.create({ + data: { + id: response.data.result?.response?.content[0].userId, + name: signupDto.name, + email: signupDto.email, + password: hashedPassword, + image: imageUrl } }); - throw new HttpException(err.response || "Wallet service not running", err.response?.status || err.status || 500); - } - try { - // Forward to marketplace portal for creation in marketplace - if(!process.env.MARKETPLACE_PORTAL_URL) - throw new HttpException("Marketplace service URL not defined", 500); - - const url = process.env.MARKETPLACE_PORTAL_URL; - const endpoint = url + `/api/admin/${admin.id}`; - - await axios.post(endpoint); - } catch(err) { - await this.prisma.admin.delete({ - where: { - id: admin.id + try { + // Forward to wallet service for creation of wallet + if(!process.env.WALLET_SERVICE_URL) + throw new HttpException("Wallet service URL not defined", 500); + + const url = process.env.WALLET_SERVICE_URL; + const endpoint = url + `/api/wallet/create`; + const reqBody = { + userId: admin.id, + type: 'ADMIN', + credits: 0 } - }); - throw new HttpException(err.response || "Marketplace service not running", err.response?.status || err.status || 500); + await axios.post(endpoint, reqBody); + } catch(err) { + await this.prisma.admin.delete({ + where: { + id: admin.id + } + }); + throw new HttpException(err.response || "Wallet service not running", err.response?.status || err.status || 500); + } + try { + // Forward to marketplace portal for creation in marketplace + if(!process.env.MARKETPLACE_PORTAL_URL) + throw new HttpException("Marketplace service URL not defined", 500); + + const url = process.env.MARKETPLACE_PORTAL_URL; + const endpoint = url + `/api/admin/${admin.id}`; + + await axios.post(endpoint); + } catch(err) { + await this.prisma.admin.delete({ + where: { + id: admin.id + } + }); + throw new HttpException(err.response || "Marketplace service not running", err.response?.status || err.status || 500); + } + return admin; + + } else { + throw new HttpException("User with the given email Id not present as an ADMIN in User Service", 400); } - return admin; } async login(loginDto: AdminLoginDto) { - const admin = await this.prisma.admin.findUnique({ + let admin = await this.prisma.admin.findUnique({ where: { email: loginDto.email } }); + if (admin == null) { throw new NotFoundException(`Admin not found`); } diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index 3e971b3..e238311 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -41,48 +41,72 @@ export class ProviderService { if(provider) throw new BadRequestException("Account with that email ID already exists"); - // Hashing the password - const hashedPassword = await this.authService.hashPassword(signupDto.password); - if(!logo) - throw new BadRequestException("Logo not uploaded"); + // check if there is a user with provider role in the user service + const baseUrl = "https://compass-dev.tarento.com/api/user/v4/user/search"; + const headers = { + 'Authorization': 'bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzRjB4UmVWNTJLcTc3R0xycnlKT2N2cXQwNVpUZTYySyJ9.ia3j2Pr-IFuioVXzerZjSNwC3HKvj-YcwjvkxOUNx0o', + 'Content-Type': 'application/json', + 'Cookie': 'connect.sid=s%3AXThrvZBYAza6cyO1AN6v2_6KOL5EM5cI.tQhpI4ryxMsIAvhAW6A%2Fkc9pAr5sxoC41PnTwUemWP0' + }; + + const data = { + "request": { + "filters": { + "profileDetails.personalDetails.primaryEmail": signupDto.email + } + } + }; - // upload the image to minio - const imageLink = await uploadFile(`provider/${signupDto.orgName.replaceAll(" ", "_")}/logo`, logo.buffer) + let response = await axios.post(baseUrl, data, {headers}); + + if(response.data.result?.response?.content[0]?.organisations[0]?.roles?.filter(role => role == "CONTENT_CREATOR").length != 0) { + // Hashing the password + const hashedPassword = await this.authService.hashPassword(signupDto.password); - // Create an entry in the database - provider = await this.prisma.provider.create({ - data: { - name: signupDto.name, - email: signupDto.email, - password: hashedPassword, - paymentInfo: signupDto.paymentInfo ? JSON.parse(signupDto.paymentInfo) : undefined, - orgName: signupDto.orgName, - orgLogo: imageLink, - phone: signupDto.phone - } - }); - try { - // Forward to wallet service for creation of wallet - if(!process.env.WALLET_SERVICE_URL) - throw new HttpException("Wallet service URL not defined", 500); - const url = process.env.WALLET_SERVICE_URL; - const endpoint = url + `/api/wallet/create`; - const reqBody = { - userId: provider.id, - type: 'PROVIDER', - credits: 0 - } - const resp = await axios.post(endpoint, reqBody); - } catch(err) { - await this.prisma.provider.delete({ - where: { - id: provider.id + if(!logo) + throw new BadRequestException("Logo not uploaded"); + + // upload the image to minio + const imageLink = await uploadFile(`provider/${signupDto.orgName.replaceAll(" ", "_")}/logo`, logo.buffer) + + // Create an entry in the database + provider = await this.prisma.provider.create({ + data: { + id: response.data.result?.response?.content[0]?.userId, + name: signupDto.name, + email: signupDto.email, + password: hashedPassword, + paymentInfo: signupDto.paymentInfo ? JSON.parse(signupDto.paymentInfo) : undefined, + orgName: signupDto.orgName, + orgLogo: imageLink, + phone: signupDto.phone } }); - throw new HttpException(err.response || "Wallet service not running", err.response?.status || err.status || 500); + try { + // Forward to wallet service for creation of wallet + if(!process.env.WALLET_SERVICE_URL) + throw new HttpException("Wallet service URL not defined", 500); + const url = process.env.WALLET_SERVICE_URL; + const endpoint = url + `/api/wallet/create`; + const reqBody = { + userId: provider.id, + type: 'PROVIDER', + credits: 0 + } + const resp = await axios.post(endpoint, reqBody); + } catch(err) { + await this.prisma.provider.delete({ + where: { + id: provider.id + } + }); + throw new HttpException(err.response || "Wallet service not running", err.response?.status || err.status || 500); + } + return provider.id + } else { + throw new HttpException("User with the given email does not exist in the user service with the role as 'PROVIDER'", 400) } - return provider.id } async getProviderIdFromLogin(loginDto: LoginDto) { From 875115e57ab78d2311f3a64654c6a1562ffcf554 Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Thu, 4 Apr 2024 13:46:47 +0530 Subject: [PATCH 82/88] minor change --- env-example | 2 ++ src/admin/admin.service.ts | 4 ++-- src/provider/provider.service.ts | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/env-example b/env-example index 6df08e7..410d29d 100644 --- a/env-example +++ b/env-example @@ -11,6 +11,8 @@ DATABASE_URL="postgresql://postgres:secret@postgres:5432/course-manager-db?sslmo WALLET_SERVICE_URL=http://wallet:4022 MARKETPLACE_PORTAL_URL=https://marketplace.backend.compass.samagra.io USER_SERVICE_URL= +USER_SERVICE_TOKEN= +USER_SERVICE_COOKIE= # MINIO MINIO_ACCESS_KEY= diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index c490e71..f75a9fa 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -37,9 +37,9 @@ export class AdminService { // check if there is a user with admin role in the user service const baseUrl = "https://compass-dev.tarento.com/api/user/v4/user/search"; const headers = { - 'Authorization': 'bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzRjB4UmVWNTJLcTc3R0xycnlKT2N2cXQwNVpUZTYySyJ9.ia3j2Pr-IFuioVXzerZjSNwC3HKvj-YcwjvkxOUNx0o', + 'Authorization': 'bearer ' + process.env.USER_SERVICE_TOKEN, 'Content-Type': 'application/json', - 'Cookie': 'connect.sid=s%3AXThrvZBYAza6cyO1AN6v2_6KOL5EM5cI.tQhpI4ryxMsIAvhAW6A%2Fkc9pAr5sxoC41PnTwUemWP0' + 'Cookie': process.env.USER_SERVICE_COOKIE }; const data = { diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index e238311..ffa6c0e 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -45,9 +45,9 @@ export class ProviderService { // check if there is a user with provider role in the user service const baseUrl = "https://compass-dev.tarento.com/api/user/v4/user/search"; const headers = { - 'Authorization': 'bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIzRjB4UmVWNTJLcTc3R0xycnlKT2N2cXQwNVpUZTYySyJ9.ia3j2Pr-IFuioVXzerZjSNwC3HKvj-YcwjvkxOUNx0o', + 'Authorization': 'bearer ' + process.env.USER_SERVICE_TOKEN, 'Content-Type': 'application/json', - 'Cookie': 'connect.sid=s%3AXThrvZBYAza6cyO1AN6v2_6KOL5EM5cI.tQhpI4ryxMsIAvhAW6A%2Fkc9pAr5sxoC41PnTwUemWP0' + 'Cookie': process.env.USER_SERVICE_COOKIE }; const data = { From 7a2b88aba8b508cc44c1c6810cd60de3f750ff11 Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Fri, 5 Apr 2024 10:08:34 +0530 Subject: [PATCH 83/88] update env vars --- src/admin/admin.service.ts | 2 +- src/provider/provider.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/admin/admin.service.ts b/src/admin/admin.service.ts index f75a9fa..fbd99cc 100644 --- a/src/admin/admin.service.ts +++ b/src/admin/admin.service.ts @@ -35,7 +35,7 @@ export class AdminService { } // check if there is a user with admin role in the user service - const baseUrl = "https://compass-dev.tarento.com/api/user/v4/user/search"; + const baseUrl = process.env.USER_SERVICE_URL || ""; const headers = { 'Authorization': 'bearer ' + process.env.USER_SERVICE_TOKEN, 'Content-Type': 'application/json', diff --git a/src/provider/provider.service.ts b/src/provider/provider.service.ts index ffa6c0e..8a48198 100644 --- a/src/provider/provider.service.ts +++ b/src/provider/provider.service.ts @@ -43,7 +43,7 @@ export class ProviderService { // check if there is a user with provider role in the user service - const baseUrl = "https://compass-dev.tarento.com/api/user/v4/user/search"; + const baseUrl = process.env.USER_SERVICE_URL || ""; const headers = { 'Authorization': 'bearer ' + process.env.USER_SERVICE_TOKEN, 'Content-Type': 'application/json', From a1193400677b88e51af59607a4a29bdc74e68d3f Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Mon, 8 Apr 2024 11:23:47 +0530 Subject: [PATCH 84/88] Max upload image size --- src/provider/provider.controller.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index b826d25..e88b4ec 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -186,7 +186,11 @@ export class ProviderController { @ApiOperation({ summary: 'add new course' }) @ApiResponse({ status: HttpStatus.CREATED, type: ProviderCourseResponse }) @Post("/:providerId/course") - @UseInterceptors(FileInterceptor('image')) + @UseInterceptors(FileInterceptor('image', { + limits: { + fileSize: 1024 * 1024 * 6, + } + })) // add new course async addCourse( @Param("providerId", ParseUUIDPipe) providerId: string, From 4098fe9c0416a44b358dbbe03e538050f71b835e Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Wed, 17 Apr 2024 13:02:34 +0530 Subject: [PATCH 85/88] Added log messages and seed data --- prisma/seed.ts | 49 ++++++++++++++++++++++++++++- src/admin/admin.controller.ts | 34 ++++++++++---------- src/course/course.controller.ts | 14 ++++----- src/provider/provider.controller.ts | 28 ++++++++--------- 4 files changed, 86 insertions(+), 39 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 23037b3..301b31b 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -372,7 +372,54 @@ async function main() { }]), author: "James Franco", verificationStatus: CourseVerificationStatus.PENDING, - }, { + }, + { + courseId: "999e4567-e89b-42d3-a456-556642440055", + providerId: provider1.id, + title: "Architecture", + description: "This course covers all the fundamentals needed for building aesthetic architectures", + courseLink: "https://udemy.com/courses/jQKsLpm", + imageLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", + credits: 160, + language: ["english", "hindi"], + avgRating: 3.5, + competency: JSON.stringify([{ + "id": 1, + "name": "Floor Planning and Mapping", + "levels": [ + { + "id": 2, + "levelNumber": 2, + "name": "Level 2" + }, { + "id": 3, + "levelNumber": 3, + "name": "Level 3" + } + ] + }, { + "id": 3, + "name": "Earth core concepts", + "levels": [ + { + "id": 1, + "levelNumber": 1, + "name": "Level 1" + }, { + "id": 2, + "levelNumber": 2, + "name": "Level 2" + }, { + "id": 3, + "levelNumber": 3, + "name": "Level 3" + } + ] + }]), + author: "James Franco", + verificationStatus: CourseVerificationStatus.ACCEPTED, + }, + { courseId: "123e4567-e89b-42d3-a456-556642440056", providerId: provider1.id, title: "Introduction to Compiler Engineering", diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 9a52bce..bb09905 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -51,7 +51,7 @@ export class AdminController { } }); } catch (err) { - this.logger.error(`Failed to sign up the admin with the given credentials`); + this.logger.error(`Failed to sign up the admin with the given credentials: `, err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -85,7 +85,7 @@ export class AdminController { } }); } catch (err) { - this.logger.error(`Failed to login the admin with the given credentials`); + this.logger.error(`Failed to login the admin with the given credentials: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -111,7 +111,7 @@ export class AdminController { data: providers }); } catch (err) { - this.logger.error(`Failed to retreive all the providers' information`); + this.logger.error(`Failed to retreive all the providers' information: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -140,7 +140,7 @@ export class AdminController { data: providers }); } catch (err) { - this.logger.error(`Failed to retreive all the providers' information for settlement`); + this.logger.error(`Failed to retreive all the providers' information for settlement: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -165,7 +165,7 @@ export class AdminController { message: "Settlement done for the provider", }); } catch (err) { - this.logger.error(`Failed to retreive all the providers' information for settlement`); + this.logger.error(`Failed to retreive all the providers' information for settlement: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -193,7 +193,7 @@ export class AdminController { data: provider }); } catch (err) { - this.logger.error(`Failed to retrieve the provider profile information`); + this.logger.error(`Failed to retrieve the provider profile information: `,err.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -222,7 +222,7 @@ export class AdminController { data: updatedProfile }); } catch (err) { - this.logger.error(`Failed to retrieve the provider profile information`); + this.logger.error(`Failed to retrieve the provider profile information: `,err.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -248,7 +248,7 @@ export class AdminController { data: response.id }); } catch (err) { - this.logger.error(`Failed to verify the provider with id ${providerId}`); + this.logger.error(`Failed to verify the provider with id ${providerId}: `,err.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -277,7 +277,7 @@ export class AdminController { } }); } catch (err) { - this.logger.error(`Failed to reject the provider account with id ${providerId}`); + this.logger.error(`Failed to reject the provider account with id ${providerId}: `,err.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -303,7 +303,7 @@ export class AdminController { data: courses }); } catch (err) { - this.logger.error(`Failed to retrieve all courses`); + this.logger.error(`Failed to retrieve all courses: `,err.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -331,7 +331,7 @@ export class AdminController { data: course }); } catch (err) { - this.logger.error(`Failed to retrieve the course for the courseId ${courseId}`); + this.logger.error(`Failed to retrieve the course for the courseId ${courseId}: `,err.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -360,7 +360,7 @@ export class AdminController { data: course }); } catch (err) { - this.logger.error(`Failed to accept the course for the courseId ${courseId}`); + this.logger.error(`Failed to accept the course for the courseId ${courseId}: `,err.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -388,7 +388,7 @@ export class AdminController { data: course }); } catch (err) { - this.logger.error(`Failed to reject the course with the courseId ${courseId}`); + this.logger.error(`Failed to reject the course with the courseId ${courseId}: `,err.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -416,7 +416,7 @@ export class AdminController { data: course }); } catch (err) { - this.logger.error(`Failed to delete the course with the courseId ${courseId}`); + this.logger.error(`Failed to delete the course with the courseId ${courseId}: `,err.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -444,7 +444,7 @@ export class AdminController { data: transactions }); } catch (err) { - this.logger.error(`Failed to fetch the transactions between admin and consumers`); + this.logger.error(`Failed to fetch the transactions between admin and consumers: `,err.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -474,7 +474,7 @@ export class AdminController { data: provider }); } catch (err) { - this.logger.error(`Failed to add credits to the provider`); + this.logger.error(`Failed to add credits to the provider: `,err.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -504,7 +504,7 @@ export class AdminController { data: provider }); } catch (err) { - this.logger.error(`Failed to remove credits from the provider`); + this.logger.error(`Failed to remove credits from the provider: `,err.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ diff --git a/src/course/course.controller.ts b/src/course/course.controller.ts index db2a5f4..6d279e9 100644 --- a/src/course/course.controller.ts +++ b/src/course/course.controller.ts @@ -35,7 +35,7 @@ export class CourseController { data: courses }) } catch (err) { - this.logger.error(`Failed to retreive the courses' information`); + this.logger.error(`Failed to retreive the courses' information: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -66,7 +66,7 @@ export class CourseController { data: courses }) } catch (err) { - this.logger.error(`Failed to fetch courses`); + this.logger.error(`Failed to fetch courses: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -96,7 +96,7 @@ export class CourseController { data: courses }) } catch (err) { - this.logger.error(`Failed to fetch courses`); + this.logger.error(`Failed to fetch courses: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -126,7 +126,7 @@ export class CourseController { data: course }) } catch (err) { - this.logger.error(`Failed to retreive the course`); + this.logger.error(`Failed to retreive the course: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -157,7 +157,7 @@ export class CourseController { data: response }) } catch (err) { - this.logger.error(`Failed to record the purchase`); + this.logger.error(`Failed to record the purchase: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -188,7 +188,7 @@ export class CourseController { message: "feedback successful" }) } catch (err) { - this.logger.error(`Failed to record the feedback`); + this.logger.error(`Failed to record the feedback: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -218,7 +218,7 @@ export class CourseController { data: filteredCourses }); } catch (err) { - this.logger.error(`Failed to filter the courses`); + this.logger.error(`Failed to filter the courses: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ diff --git a/src/provider/provider.controller.ts b/src/provider/provider.controller.ts index e88b4ec..513b4d4 100644 --- a/src/provider/provider.controller.ts +++ b/src/provider/provider.controller.ts @@ -46,7 +46,7 @@ export class ProviderController { data: found }) } catch (err) { - this.logger.error(`Failed to check provider`); + this.logger.error(`Failed to check provider: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ statusCode, @@ -79,7 +79,7 @@ export class ProviderController { } }) } catch (err) { - this.logger.error(`Failed to create new provider account`); + this.logger.error(`Failed to create new provider account: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -111,7 +111,7 @@ export class ProviderController { } }) } catch (err) { - this.logger.error(`Failed to log in`); + this.logger.error(`Failed to log in: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -141,7 +141,7 @@ export class ProviderController { data : provider }) } catch (err) { - this.logger.error(`Failed to retreive provider profile`); + this.logger.error(`Failed to retreive provider profile: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -173,7 +173,7 @@ export class ProviderController { message: "account updated successfully", }) } catch (err) { - this.logger.error(`Failed to update provider profile`); + this.logger.error(`Failed to update provider profile: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -210,7 +210,7 @@ export class ProviderController { data: course }) } catch (err) { - this.logger.error(`Failed to add the course`); + this.logger.error(`Failed to add the course: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -240,7 +240,7 @@ export class ProviderController { data: courses }) } catch (err) { - this.logger.error(`Failed to fetch the courses`); + this.logger.error(`Failed to fetch the courses: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -270,7 +270,7 @@ export class ProviderController { data: transactionsResponse }) } catch (err) { - this.logger.error(`Failed to fetch the transactions`); + this.logger.error(`Failed to fetch the transactions: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -300,7 +300,7 @@ export class ProviderController { message: "course marked complete", }) } catch (err) { - this.logger.error(`Failed to mark the course completion`); + this.logger.error(`Failed to mark the course completion: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -333,7 +333,7 @@ export class ProviderController { message: "course edited successfully", }) } catch (err) { - this.logger.error(`Failed to update course information`); + this.logger.error(`Failed to update course information: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -364,7 +364,7 @@ export class ProviderController { message: "course deleted successfully", }) } catch (err) { - this.logger.error(`Failed to delete the course`); + this.logger.error(`Failed to delete the course: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -395,7 +395,7 @@ export class ProviderController { message: "course status changed successfully", }) } catch (err) { - this.logger.error(`Failed to change course status`); + this.logger.error(`Failed to change course status: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -426,7 +426,7 @@ export class ProviderController { data: feedbackResponse }) } catch (err) { - this.logger.error(`Failed to fetch the feedbacks`); + this.logger.error(`Failed to fetch the feedbacks: `,err.message); const {errorMessage, statusCode} = getPrismaErrorStatusAndMessage(err); res.status(statusCode).json({ @@ -457,7 +457,7 @@ export class ProviderController { message: "Successfully reset the password.", }); } catch (error) { - this.logger.error(`Failed to reset the password.`); + this.logger.error(`Failed to reset the password: `,error.message); const { errorMessage, statusCode } = getPrismaErrorStatusAndMessage(error); From 4aa722f9f955c3a2a0bde9a735892466bb11d50f Mon Sep 17 00:00:00 2001 From: kh4l1d64 <144687945+kh4l1d64@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:29:55 +0530 Subject: [PATCH 86/88] Update env-example --- env-example | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/env-example b/env-example index 410d29d..b4bc859 100644 --- a/env-example +++ b/env-example @@ -1,15 +1,15 @@ NODE_ENV=development -APP_PORT=4030 +APP_PORT= APP_NAME="Course Manager API" API_PREFIX=api -DATABASE_USERNAME=postgres -DATABASE_PASSWORD=secret +DATABASE_USERNAME= +DATABASE_PASSWORD= DATABASE_NAME=course-manager-db -DATABASE_PORT=5432 -DATABASE_URL="postgresql://postgres:secret@postgres:5432/course-manager-db?sslmode=prefer" -WALLET_SERVICE_URL=http://wallet:4022 -MARKETPLACE_PORTAL_URL=https://marketplace.backend.compass.samagra.io +DATABASE_PORT= +DATABASE_URL= +WALLET_SERVICE_URL= +MARKETPLACE_PORTAL_URL= USER_SERVICE_URL= USER_SERVICE_TOKEN= USER_SERVICE_COOKIE= From 1515a3b0a1b1b5d990417ce463c6829e8ccea49a Mon Sep 17 00:00:00 2001 From: kh4l1d64 Date: Fri, 19 Apr 2024 13:40:33 +0530 Subject: [PATCH 87/88] Sync seed data with marketplace-portal --- prisma/seed.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/prisma/seed.ts b/prisma/seed.ts index 301b31b..d6ffbf8 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -535,17 +535,17 @@ async function main() { courseId: "123e4567-e89b-42d3-a456-556642440051", status: CourseProgressStatus.COMPLETED, }, { - userId: "abaa7220-5d2e-4e05-842a-95b2c4ce1876", + userId: "890f2839-866f-4524-9eac-bebe0d35d607", feedback: "Instructor is very friendly", rating: 4, status: CourseProgressStatus.COMPLETED, courseCompletionScore: 100, courseId: "123e4567-e89b-42d3-a456-556642440050" }, { - userId: "abaa7220-5d2e-4e05-842a-95b2c4ce1876", + userId: "890f2839-866f-4524-9eac-bebe0d35d607", courseId: "123e4567-e89b-42d3-a456-556642440051" }, { - userId: "abaa7220-5d2e-4e05-842a-95b2c4ce1876", + userId: "890f2839-866f-4524-9eac-bebe0d35d607", feedback: "Some more real world applications could be discussed", rating: 3, status: CourseProgressStatus.COMPLETED, From 5597acd3abb8a0ccb3aecdb18fd7b04c58d7cd8e Mon Sep 17 00:00:00 2001 From: VamshiBatta07 Date: Wed, 17 Jul 2024 10:37:34 +0530 Subject: [PATCH 88/88] Creating views for data visualization. --- env-example | 5 +- package-lock.json | 125 +++++ package.json | 3 +- prisma/scripts/createViewQueries.ts | 96 ++++ prisma/scripts/moveViewsQueries.ts | 103 +++++ prisma/seed.ts | 686 ++++++++++++++++------------ 6 files changed, 730 insertions(+), 288 deletions(-) create mode 100644 prisma/scripts/createViewQueries.ts create mode 100644 prisma/scripts/moveViewsQueries.ts diff --git a/env-example b/env-example index b4bc859..0866388 100644 --- a/env-example +++ b/env-example @@ -5,9 +5,10 @@ API_PREFIX=api DATABASE_USERNAME= DATABASE_PASSWORD= -DATABASE_NAME=course-manager-db +DATABASE_NAME= +TELEMETRY_DATABASE_NAME= DATABASE_PORT= -DATABASE_URL= +DATABASE_URL=postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@172.17.0.1:5432/${DATABASE_NAME}?schema=public WALLET_SERVICE_URL= MARKETPLACE_PORTAL_URL= USER_SERVICE_URL= diff --git a/package-lock.json b/package-lock.json index 2139230..aa5ad6a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "lodash": "^4.17.21", "minio": "^7.1.3", "nestjs-prisma": "^0.22.0", + "pg": "^8.12.0", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", @@ -7124,6 +7125,87 @@ "node": ">=8" } }, + "node_modules/pg": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz", + "integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==", + "dependencies": { + "pg-connection-string": "^2.6.4", + "pg-pool": "^3.6.2", + "pg-protocol": "^1.6.1", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz", + "integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz", + "integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", + "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -7224,6 +7306,41 @@ "node": ">=4" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -7949,6 +8066,14 @@ "node": ">=6" } }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", diff --git a/package.json b/package.json index 0815101..a7b5950 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", "rxjs": "^7.2.0", - "swagger-ui-express": "^5.0.0" + "swagger-ui-express": "^5.0.0", + "pg": "^8.12.0" }, "devDependencies": { "@nestjs/cli": "^9.0.0", diff --git a/prisma/scripts/createViewQueries.ts b/prisma/scripts/createViewQueries.ts new file mode 100644 index 0000000..aa88aa3 --- /dev/null +++ b/prisma/scripts/createViewQueries.ts @@ -0,0 +1,96 @@ +export const createViewQueries = [ + ` + DROP VIEW IF EXISTS metrics_3cp_overall; + `, + ` + DROP VIEW IF EXISTS metrics_courses_overall + `, + ` + DROP VIEW IF EXISTS metrics_user_courses + `, + ` + DROP VIEW IF EXISTS user_courses_mapping + `, + ` + DROP VIEW IF EXISTS telemetry_3cp_details + `, + ` + DROP VIEW IF EXISTS telemetry_3cp_courses + `, + ` + CREATE VIEW metrics_3cp_overall AS + SELECT + (SELECT COUNT(*) FROM "Provider" WHERE status = 'VERIFIED') AS "active3CPs", + (SELECT COUNT(*) FROM "Provider" WHERE status = 'PENDING') AS "pending3CPs", + (SELECT COUNT(*) FROM "Provider" WHERE status = 'REJECTED') AS "rejected3CPs", + (SELECT COUNT(*) FROM "Provider") AS "total3CPs" + `, + + ` + CREATE VIEW metrics_courses_overall AS + SELECT + (SELECT COUNT(*) FROM "Course" WHERE "verificationStatus" = 'ACCEPTED') AS "activeCourses", + (SELECT COUNT(*) FROM "Course" WHERE "verificationStatus" = 'PENDING') AS "pendingCourses", + (SELECT COUNT(*) FROM "Course" WHERE "verificationStatus" = 'REJECTED') AS "rejectedCourses", + (SELECT COUNT(*) FROM "Course") AS "totalCourses", + (SELECT SUM("avgRating")/ COUNT(*) FROM "Course" WHERE "avgRating" > 0) AS "avgActiveCourseRating", + (SELECT COUNT(*) FROM "UserCourse" WHERE status = 'COMPLETED') AS "coursesCompleted", + (SELECT COUNT(*) FROM "UserCourse" WHERE status = 'IN_PROGRESS') AS "coursesInProgress" + `, + + ` + CREATE VIEW metrics_user_courses AS + SELECT + uc."userId", + COUNT(DISTINCT uc."courseId") AS "coursesRegisteredCount", + COUNT(*) FILTER (WHERE uc.status = 'IN_PROGRESS') AS "coursesInProgressCount", + COUNT(*) FILTER (WHERE uc.status = 'COMPLETED') AS "coursesCompletedCount" + FROM + "UserCourse" uc + GROUP BY + uc."userId" + `, + ` + CREATE VIEW user_courses_mapping AS + SELECT + uc."userId", + c."courseId", + c.title, + uc.status + FROM + "UserCourse" uc + JOIN + "Course" c ON uc."courseId" = c."courseId" + GROUP BY + uc."userId", c."courseId", uc.status + `, + ` + CREATE VIEW telemetry_3cp_details AS + SELECT + p.id AS "3cpId", + p.name, + p."orgName" AS "organizationName", + p.status, + p."rejectionReason" + FROM + "Provider" p + GROUP BY + p.id, p.name + `, + ` + CREATE VIEW telemetry_3cp_courses AS + SELECT + c."providerId" AS "3cpId", + c."courseId", + c.title AS "courseName", + c."avgRating" AS rating, + c."verificationStatus", + COUNT(DISTINCT uc."userId") AS "endUsersCount" + FROM + "Course" c + LEFT JOIN + "UserCourse" uc ON c."courseId" = uc."courseId" + GROUP BY + c."courseId", c.title + ` +]; \ No newline at end of file diff --git a/prisma/scripts/moveViewsQueries.ts b/prisma/scripts/moveViewsQueries.ts new file mode 100644 index 0000000..1710165 --- /dev/null +++ b/prisma/scripts/moveViewsQueries.ts @@ -0,0 +1,103 @@ +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 = 'course_manager_server' + ) THEN + EXECUTE 'CREATE SERVER course_manager_server + FOREIGN DATA WRAPPER postgres_fdw + OPTIONS (host ''localhost'', dbname ''course-manager-db'', port ''5432'')'; + END IF; + END $$; + `, + ` + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_user_mappings + WHERE srvname = 'course_manager_server' AND usename = 'supercompass' + ) THEN + EXECUTE 'CREATE USER MAPPING FOR supercompass + SERVER course_manager_server + OPTIONS (user ''supercompass'', password ''tCzPKM47gE5ixVjn'')'; + END IF; + END $$; + `, + ` + CREATE FOREIGN TABLE metrics_3cp_overall ( + "active3CPs" BIGINT, + "pending3CPs" BIGINT, + "rejected3CPs" BIGINT, + "total3CPs" BIGINT + ) + SERVER course_manager_server + OPTIONS (schema_name 'public', table_name 'metrics_3cp_overall'); + `, + ` + CREATE FOREIGN TABLE metrics_courses_overall ( + "activeCourses" bigint, + "pendingCourses" bigint, + "rejectedCourses" bigint, + "totalCourses" bigint, + "avgActiveCourseRating" double precision, + "coursesCompleted" bigint, + "coursesInProgress" bigint + ) + SERVER course_manager_server + OPTIONS (schema_name 'public', table_name 'metrics_courses_overall'); + `, + ` + CREATE FOREIGN TABLE metrics_user_courses ( + "userId" text, + "coursesRegisteredCount" bigint, + "coursesInProgressCount" bigint, + "coursesCompletedCount" bigint + ) + SERVER course_manager_server + OPTIONS (schema_name 'public', table_name 'metrics_user_courses'); + `, + ` + CREATE FOREIGN TABLE user_courses_mapping ( + "userId" text, + "courseId" text, + "title" text, + "status" text + ) + SERVER course_manager_server + OPTIONS (schema_name 'public', table_name 'user_courses_mapping'); + `, + ` + CREATE FOREIGN TABLE telemetry_3cp_details ( + "3cpId" text, + "name" text, + "organizationName" text, + "status" "ProviderStatus", + "rejectionReason" text + ) + SERVER course_manager_server + OPTIONS (schema_name 'public', table_name 'telemetry_3cp_details'); + `, + ` + CREATE FOREIGN TABLE telemetry_3cp_courses ( + "3cpId" text, + "courseId" text, + "courseName" text, + "rating" double precision, + "verificationStatus" text, + "endUsersCount" bigint + ) + SERVER course_manager_server + OPTIONS (schema_name 'public', table_name 'telemetry_3cp_courses'); + `, +]; \ No newline at end of file diff --git a/prisma/seed.ts b/prisma/seed.ts index d6ffbf8..b2d9c20 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,15 +1,22 @@ -import { CourseProgressStatus, CourseStatus, CourseVerificationStatus, PrismaClient, ProviderStatus } from '@prisma/client' +import { Logger } from '@nestjs/common'; +import { CourseProgressStatus, CourseStatus, CourseVerificationStatus, Prisma, PrismaClient, ProviderStatus } from '@prisma/client' import * as bcrypt from 'bcrypt'; -const prisma = new PrismaClient() +import * as fs from "fs"; +import { createViewQueries } from "./scripts/createViewQueries"; +import { copyViewQueries } from "./scripts/moveViewsQueries" +const prisma = new PrismaClient(); +const { Client } = require('pg'); +const telemetryDbName = process.env.TELEMETRY_DATABASE_NAME; -async function main() { +async function seed() { const saltRounds = 10; - const hashedPassword = await bcrypt.hash("123456", saltRounds); + const hashedPassword = await bcrypt.hash("Dilu@123", saltRounds); + const hashedPassword1 = await bcrypt.hash("Favas@456", saltRounds); const hashedPassword2 = await bcrypt.hash("Udemy@9812", saltRounds); const hashedPassword3 = await bcrypt.hash("Coursera@999", saltRounds); const hashedPassword4 = await bcrypt.hash("lern@999", saltRounds); - const response = await prisma.provider.create({ + const provider = await prisma.provider.create({ data: { id: "123e4567-e89b-42d3-a456-556642440010", name: "Vijay Salgaonkar", @@ -84,66 +91,51 @@ async function main() { data: [{ courseId: "123e4567-e89b-42d3-a456-556642440050", providerId: provider1.id, - title: "NestJS Complete", - description: "Build full featured backend APIs incredibly quickly with Nest, TypeORM, and Typescript. Includes testing and deployment!", + title: "Comprehensive Floor Inspection Techniques", + description: "Develop skills for thorough and accurate floor inspections.", courseLink: "https://www.udemy.com/course/nestjs-the-complete-developers-guide/", imageLink: "https://courses.nestjs.com/img/logo.svg", credits: 4, language: ["en"], competency: JSON.stringify([{ - "id": 1, - "name": "API Development", - "levels": [ - { - "id": 1, - "levelNumber": 1, - "name": "Basic" - }, { - "id": 2, - "levelNumber": 2, - "name": "Intermediate" - }, { - "id": 3, - "levelNumber": 3, - "name": "Advanced" - } - ] + "id": 1, + "name": "Floor Planning and Mapping", + "levels": [ + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] }, { - "id": 2, - "name": "Typescript", - "levels": [ - { - "id": 4, - "levelNumber": 1, - "name": "Basic" - }, { - "id": 5, - "levelNumber": 2, - "name": "Intermediate" - }, { - "id": 6, - "levelNumber": 3, - "name": "Advanced" - } - ] + "id": 8, + "name": "Floor Inspection", + "levels": [ + { + "levelNumber": 1, + "name": "Level 1", + "id": 1 + }, + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] }, { - "id": 3, - "name": "Backend engineering", - "levels": [ - { - "id": 7, - "levelNumber": 1, - "name": "Basic" - }, { - "id": 8, - "levelNumber": 2, - "name": "Intermediate" - }, { - "id": 9, - "levelNumber": 3, - "name": "Advanced" - } - ] + "id": 7, + "name": "Survey", + "levels": null } ]), author: "Stephen Grider", @@ -153,36 +145,48 @@ async function main() { }, { courseId: "123e4567-e89b-42d3-a456-556642440051", providerId: provider1.id, - title: "Graphic Design Masterclass", - description: "The Ultimate Graphic Design Course Which Covers Photoshop, Illustrator, InDesign, Design Theory, Branding & Logo Design", + title: "Advanced Floor Planning and Inspection", + description: "Master the skills of creating detailed floor plans and conducting thorough floor inspections.", courseLink: "https://www.udemy.com/course/graphic-design-masterclass-everything-you-need-to-know/", imageLink: "https://www.unite.ai/wp-content/uploads/2023/05/emily-bernal-v9vII5gV8Lw-unsplash.jpg", credits: 5, language: ["en"], competency: JSON.stringify([{ - "id": 1, - "name": "Photoshop", - "levels": [{ - "id": 2, - "levelNumber": 2, - "name": "Intermediate" - }, { - "id": 3, - "levelNumber": 3, - "name": "Advanced" - } - ] - }, { - "id": 2, - "name": "Understanding brand", - "levels": [ - { - "id": 4, - "levelNumber": 1, - "name": "Basic" - } - ] - } + "id": 8, + "name": "Floor Inspection", + "levels": [ + { + "levelNumber": 1, + "name": "Level 1", + "id": 1 + }, + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] + }, { + "id": 1, + "name": "Floor Planning and Mapping", + "levels": [ + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] + } ]), author: "Lindsay Marsh", startDate: new Date("2023-05-01").toISOString(), @@ -191,139 +195,137 @@ async function main() { }, { courseId: "123e4567-e89b-42d3-a456-556642440052", providerId: provider1.id, - title: "Python for Data Science", - description: "Learn how to use NumPy, Pandas, Seaborn , Matplotlib , Plotly , Scikit-Learn , Machine Learning, Tensorflow , and more", + title: "Strategic Coverage and Survey Techniques", + description: "Learn to plan effective surveillance coverage and conduct comprehensive site surveys.", courseLink: "https://www.udemy.com/course/python-for-data-science-and-machine-learning-bootcamp/", imageLink: "https://blog.imarticus.org/wp-content/uploads/2021/12/learn-Python-for-data-science.jpg", credits: 2, language: ["en"], competency: JSON.stringify([{ - "id": 1, - "name": "Statistics", - "levels": [ - { - "id": 1, - "levelNumber": 1, - "name": "Basic" - } - ] - }, { - "id": 2, - "name": "Machine Learning", - "levels": [ - { - "id": 4, - "levelNumber": 1, - "name": "Basic" - }, { - "id": 5, - "levelNumber": 2, - "name": "Intermediate" - }, { - "id": 6, - "levelNumber": 3, - "name": "Advanced" - } - ] - }, { - "id": 3, - "name": "MySQL", - "levels": [{ - "id": 7, - "levelNumber": 1, - "name": "Basic" - }] - } + "id": 2, + "name": "Coverage and surveillance", + "levels": [ + { + "levelNumber": 1, + "name": "Level 1", + "id": 1 + }, + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] + }, { + "id": 7, + "name": "Survey", + "levels": null + } + ]), author: "Jose Portilla", }, { courseId: "123e4567-e89b-42d3-a456-556642440053", - providerId: response.id, - title: "Microsoft Excel", - description: "Excel with this A-Z Microsoft Excel Course. Microsoft Excel 2010, 2013, 2016, Excel 2019 and Microsoft/Office 365/2023", + providerId: provider.id, + title: "Earth Core and Land Assessment Essentials", + description: "Understand the core concepts of earth cutting and digging along with documenting land assessments.", courseLink: "https://www.udemy.com/course/microsoft-excel-2013-from-beginner-to-advanced-and-beyond/", imageLink: "https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Microsoft_Excel_2013-2019_logo.svg/587px-Microsoft_Excel_2013-2019_logo.svg.png", credits: 4, language: ["en"], competency: JSON.stringify([{ - "id": 1, - "name": "Excel", - "levels": [ - { - "id": 1, - "levelNumber": 1, - "name": "Basic" - }, { - "id": 5, - "levelNumber": 2, - "name": "Intermediate" - }, { - "id": 6, - "levelNumber": 3, - "name": "Advanced" - }, { - "id": 9, - "levelNumber": 4, - "name": "Export" - } - ] - }]), + "id": 3, + "name": "Earth core concepts", + "levels": [ + { + "levelNumber": 1, + "name": "Level 1", + "id": 1 + }, + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] + }, { + "id": 4, + "name": "Assessment Documentations", + "levels": [ + { + "levelNumber": 1, + "name": "Level 1", + "id": 1 + }, + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] + }]), author: "Kyle Pew", startDate: new Date("2024-05-01").toISOString(), }, { courseId: "123e4567-e89b-42d3-a456-556642440054", providerId: provider1.id, - title: "Learn DevOps & Kubernetes", - description: "This course enables anyone to get started with devops engineering.", + title: "Comprehensive Spatial and Interior Design", + description: "Gain holistic spatial insight and learn to design visually pleasing and functional interior spaces.", courseLink: "https://www.udemy.com/course/devops-with-docker-kubernetes-and-azure-devops/", imageLink: "https://img-c.udemycdn.com/course/240x135/5030480_b416_2.jpg", credits: 120, language: ["english", "hindi"], competency: JSON.stringify([{ - "id": 1, - "name": "Docker", - "levels": [ - { - "id": 1, - "levelNumber": 1, - "name": "Basic" - }, { - "id": 6, + "id": 19, + "name": "Comprehensive Spatial Insight", + "levels": [ + { + "levelNumber": 1, + "name": "Level 1", + "id": 1 + }, + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { "levelNumber": 3, - "name": "Advanced" + "name": "Level 3", + "id": 3 } - ] - }, { - "id": 2, - "name": "Kubernetes", - "levels": [ - { - "id": 4, - "levelNumber": 1, - "name": "Basic" - }, { - "id": 5, - "levelNumber": 2, - "name": "Intermediate" - } - ] - }, { - "id": 3, - "name": "Orchestration", - "levels": [{ - "id": 7, - "levelNumber": 2, - "name": "Intermediate" - }] - }, { - "id": 4, - "name": "Micro Architecture", - "levels": [{ - "id": 7, - "levelNumber": 1, - "name": "Basic" - }] - } + ] + }, { + "id": 23, + "name": "Interior Engineering", + "levels": [ + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] + } ]), author: "Jason Frig", startDate: new Date("2023-06-01"), @@ -334,42 +336,58 @@ async function main() { }, { courseId: "123e4567-e89b-42d3-a456-556642440055", providerId: provider1.id, - title: "Introduction to Programming", - description: "This course covers all the fundamentals of programming", + title: "Integrated Survey and Documentation Mastery", + description: "Develop expertise in site surveying and documenting assessments for land evaluation.", courseLink: "https://udemy.com/courses/jQKsLpm", imageLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, language: ["english", "hindi"], avgRating: 3.5, competency: JSON.stringify([{ - "id": 1, - "name": "Logical Thinking", - "levels": [ - { - "id": 1, - "levelNumber": 1, - "name": "Basic" - }, { - "id": 6, - "levelNumber": 3, - "name": "Advanced" - } - ] - }, { - "id": 2, - "name": "Python", - "levels": [ - { - "id": 4, - "levelNumber": 1, - "name": "Basic" - }, { - "id": 5, - "levelNumber": 2, - "name": "Intermediate" - } - ] - }]), + "id": 4, + "name": "Assessment Documentations", + "levels": [ + { + "levelNumber": 1, + "name": "Level 1", + "id": 1 + }, + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] + }, { + "id": 2, + "name": "Coverage and surveillance", + "levels": [ + { + "levelNumber": 1, + "name": "Level 1", + "id": 1 + }, + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] + }, { + "id": 7, + "name": "Survey", + "levels": null + }]), author: "James Franco", verificationStatus: CourseVerificationStatus.PENDING, }, @@ -422,34 +440,44 @@ async function main() { { courseId: "123e4567-e89b-42d3-a456-556642440056", providerId: provider1.id, - title: "Introduction to Compiler Engineering", - description: "This course covers how compilers are built and also teaches you about how to create custom programming languages", + title: "Holistic Site and Interior Planning", + description: "Master site surveys, floor planning, and interior engineering to create functional and visually appealing spaces.", courseLink: "https://udemy.com/courses/jQKsLpm", imageLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, language: ["english", "hindi"], competency: JSON.stringify([{ + "id": 7, + "name": "Survey", + "levels": null + }, { "id": 1, - "name": "Compiler Design", + "name": "Floor Planning and Mapping", "levels": [ { - "id": 1, "levelNumber": 2, - "name": "Intermediate" - }, { - "id": 6, - "levelNumber": 3, - "name": "Advanced" - } + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } ] }, { - "id": 2, - "name": "LLVM", + "id": 23, + "name": "Interior Engineering", "levels": [ { - "id": 4, - "levelNumber": 4, - "name": "Expert" + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 } ] }]), @@ -461,37 +489,73 @@ async function main() { }, { courseId: "123e4567-e89b-42d3-a456-556642440057", providerId: provider1.id, - title: "Introduction to Compiler Engineering 2", - description: "This course covers how compilers are built and also teaches you about how to create custom programming languages", + title: "Complete Earth Core and Spatial Engineering", + description: "Learn the fundamentals of earth core concepts, floor inspection, and achieve absolute clarity in spatial and structural planning.", courseLink: "https://udemy.com/courses/jQKsLpm", imageLink: "https://udemy.com/courses/jQKsLpm/images/cover2.jpg", credits: 160, language: ["english", "hindi"], competency: JSON.stringify([{ - "id": 1, - "name": "Compiler Design", - "levels": [ - { - "id": 1, - "levelNumber": 2, - "name": "Intermediate" - }, { - "id": 6, - "levelNumber": 3, - "name": "Advanced" - } - ] - }, { - "id": 2, - "name": "LLVM", - "levels": [ - { - "id": 4, - "levelNumber": 4, - "name": "Expert" - } - ] - }]), + "id": 3, + "name": "Earth core concepts", + "levels": [ + { + "levelNumber": 1, + "name": "Level 1", + "id": 1 + }, + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] + }, { + "id": 8, + "name": "Floor Inspection", + "levels": [ + { + "levelNumber": 1, + "name": "Level 1", + "id": 1 + }, + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] + }, { + "id": 19, + "name": "Comprehensive Spatial Insight", + "levels": [ + { + "levelNumber": 1, + "name": "Level 1", + "id": 1 + }, + { + "levelNumber": 2, + "name": "Level 2", + "id": 2 + }, + { + "levelNumber": 3, + "name": "Level 3", + "id": 3 + } + ] + }]), author: "Ramakrishna Upadrasta", startDate: new Date("2023-10-10"), endDate: new Date("2023-11-10"), @@ -499,24 +563,24 @@ async function main() { status: CourseStatus.ARCHIVED }] }) - const hashedPassword1 = await bcrypt.hash("asdfghjkl", saltRounds); + const admin = await prisma.admin.create({ data: { - name: "Sanchit Uke", - email: "sanchit@esmagico.in", - password: hashedPassword1, + name: "dilu", + email: "dilu@yopmail.com", + password: hashedPassword, image: "https://avatars.githubusercontent.com/u/46641520?v=4", - id: "123e4567-e89b-42d3-a456-556642440020", + id: "87fd80a9-63e9-4e90-81bb-4b6956c2561b", } }); const admin1 = await prisma.admin.create({ data: { - name: 'admin1', - email: "admin1@gmail.com", + name: 'favas', + email: "favas@yopmail.com", image: "https://avatars.githubusercontent.com/u/46641520?v=4", - password: hashedPassword, - id: "123e4567-e89b-42d3-a456-556642440021", + password: hashedPassword1, + id: "890f2839-866f-4524-9eac-bebe0d35d607", }, }); @@ -524,35 +588,35 @@ async function main() { // console.log("All courses: ", resp); const response3 = await prisma.userCourse.createMany({ data: [{ - userId: "4d45a9e9-4a4d-4c92-aaea-7b5abbd6ff98", + userId: "9f4611d4-ab92-4acd-b3ce-13594e362eca", feedback: "Great course", rating: 4, status: CourseProgressStatus.COMPLETED, courseCompletionScore: 100, courseId: "123e4567-e89b-42d3-a456-556642440050" }, { - userId: "4d45a9e9-4a4d-4c92-aaea-7b5abbd6ff98", + userId: "9f4611d4-ab92-4acd-b3ce-13594e362eca", courseId: "123e4567-e89b-42d3-a456-556642440051", status: CourseProgressStatus.COMPLETED, }, { - userId: "890f2839-866f-4524-9eac-bebe0d35d607", + userId: "836ba369-fc24-4464-95ec-505d61b67ef0", feedback: "Instructor is very friendly", rating: 4, status: CourseProgressStatus.COMPLETED, courseCompletionScore: 100, courseId: "123e4567-e89b-42d3-a456-556642440050" }, { - userId: "890f2839-866f-4524-9eac-bebe0d35d607", + userId: "836ba369-fc24-4464-95ec-505d61b67ef0", courseId: "123e4567-e89b-42d3-a456-556642440051" }, { - userId: "890f2839-866f-4524-9eac-bebe0d35d607", + userId: "836ba369-fc24-4464-95ec-505d61b67ef0", feedback: "Some more real world applications could be discussed", rating: 3, status: CourseProgressStatus.COMPLETED, courseCompletionScore: 100, courseId: "123e4567-e89b-42d3-a456-556642440052" }, { - userId: "0f5d0b13-8d72-46c9-a7c4-c1f7e5aa1f17", + userId: "c8a43816-5a1b-4e29-9e1f-e8ef22efc669", feedback: "Not satisfied with the content", rating: 2, status: CourseProgressStatus.COMPLETED, @@ -560,19 +624,71 @@ async function main() { courseId: "123e4567-e89b-42d3-a456-556642440052" }] }) - console.log(response) - console.log({ response1, response3, admin1, provider1, provider2, provider3, admin }); + console.log({ response1, response3, admin, admin1, provider, provider1, provider2, provider3 }); } +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"); -// execute the main function -main() - .catch((e) => { + 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(copyViewQueries); + + 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`); + } + + +// execute the functions +async function main() { + try { + await seed(); + await createViews(); + await moveViews(); + } catch (e) { console.error(e); process.exit(1); - }) - .finally(async () => { - // close Prisma Client at the end + } finally { await prisma.$disconnect(); - }); + } +} + +main();