From 7015dbe7f288e4aff63614144b24b9f5688608fd Mon Sep 17 00:00:00 2001 From: myukang Date: Tue, 5 Sep 2023 13:41:19 +0900 Subject: [PATCH 1/9] :memo: [Chore]: add api doc --- api-docs.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/api-docs.yml b/api-docs.yml index 6ac7976..26cf451 100644 --- a/api-docs.yml +++ b/api-docs.yml @@ -547,6 +547,39 @@ paths: '409': description: already exists + /reservations/{id}/check: + patch: + summary: check accepted reservation by mentee + description: | + **ROLE**\ + \ + only for `mentee` user that related with reservation + tags: + - Reservations + security: + - OwnerUser: [ ] + parameters: + - name: id + in: path + required: true + description: "reservation id" + schema: + type: integer + format: int32 + responses: + '200': + description: Updated + content: + application/json: + schema: + $ref: '#/components/schemas/ReservationGet' + '400': + description: Invalid request body + '401': + description: Unauthorized + '409': + description: already exists + /reservations/{id}/mentor_completion: patch: summary: complete reservation as mentor From 683e7004d7ecb27739bedafdfb004ccb8b1bc800 Mon Sep 17 00:00:00 2001 From: myukang Date: Tue, 5 Sep 2023 14:08:14 +0900 Subject: [PATCH 2/9] :card_file_box: [Chore]: change scheme add migration file made by 'npx prisma migrate dev' --- prisma/migrations/20230905050509_mentee_checked/migration.sql | 2 ++ prisma/schema.prisma | 1 + 2 files changed, 3 insertions(+) create mode 100644 prisma/migrations/20230905050509_mentee_checked/migration.sql diff --git a/prisma/migrations/20230905050509_mentee_checked/migration.sql b/prisma/migrations/20230905050509_mentee_checked/migration.sql new file mode 100644 index 0000000..66138c8 --- /dev/null +++ b/prisma/migrations/20230905050509_mentee_checked/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE `reservations` MODIFY `status` ENUM('REQUEST', 'ACCEPT', 'CANCEL', 'MENTEE_CHECKED', 'MENTEE_FEEDBACK', 'DONE') NOT NULL DEFAULT 'REQUEST'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 69ff3cb..41f9838 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -60,6 +60,7 @@ enum ReservationStatus { REQUEST ACCEPT CANCEL + MENTEE_CHECKED MENTEE_FEEDBACK DONE } From 6a305509f1cc117735e269200c48fe1edcf79d0f Mon Sep 17 00:00:00 2001 From: myukang Date: Tue, 5 Sep 2023 15:30:58 +0900 Subject: [PATCH 3/9] :sparkles: [Feature]: cancel status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cancel할때 MENTEE_CHECK상태도 허용하도록 변경 --- src/database/repository/reservation.repository.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/database/repository/reservation.repository.ts b/src/database/repository/reservation.repository.ts index e044163..03e9ccb 100644 --- a/src/database/repository/reservation.repository.ts +++ b/src/database/repository/reservation.repository.ts @@ -99,12 +99,13 @@ export class ReservationRepository { where: { id: reservationId }, }); /** - * NOTE: 예약은 REQUEST/ACCEPT 상태일 때만 취소할 수 있다. + * NOTE: 예약은 REQUEST/ACCEPT/MENTEE_CHECKED 상태일 때만 취소할 수 있다. * */ if ( !reservation || (reservation.status !== ReservationStatus.REQUEST && - reservation.status !== ReservationStatus.ACCEPT) + reservation.status !== ReservationStatus.ACCEPT && + reservation.status !== ReservationStatus.MENTEE_CHECKED) ) throw new BadRequestException('invalid reservation for accept'); /** From 5a8bc1460d59e613461ae0134c71ec1577af2778 Mon Sep 17 00:00:00 2001 From: myukang Date: Tue, 5 Sep 2023 15:41:13 +0900 Subject: [PATCH 4/9] :sparkles: [Feature]: cancel status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cancel할때 MENTEE_CHECK상태도 허용하도록 변경 --- src/database/repository/reservation.repository.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/database/repository/reservation.repository.ts b/src/database/repository/reservation.repository.ts index 03e9ccb..4f6e086 100644 --- a/src/database/repository/reservation.repository.ts +++ b/src/database/repository/reservation.repository.ts @@ -108,13 +108,15 @@ export class ReservationRepository { reservation.status !== ReservationStatus.MENTEE_CHECKED) ) throw new BadRequestException('invalid reservation for accept'); + /** - * NOTE: ACCEPT 상태의 예약은 멘토 혹은 Admin 만 취소할 수 있다. + * NOTE: ACCEPT/MENTEE_CHECKED 상태의 예약은 멘토 혹은 Admin 만 취소할 수 있다. */ if ( - reservation.status === ReservationStatus.ACCEPT && - role !== UserRole.ADMIN && - reservation.mentorId !== userId + (reservation.status === ReservationStatus.ACCEPT || + reservation.status === ReservationStatus.MENTEE_CHECKED) && + reservation.mentorId !== userId && + role !== UserRole.ADMIN ) throw new UnauthorizedException('user is not mentor of this reservation'); return prisma.reservation.update({ From e33a90f85286e3290f49beaad70ec63ef4b59b93 Mon Sep 17 00:00:00 2001 From: myukang Date: Tue, 5 Sep 2023 15:42:17 +0900 Subject: [PATCH 5/9] :sparkles: [Feature]: mentee completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mentee의 feedback 작성 시, mentee_checked상태도 허용하도록 변경 --- src/database/repository/reservation.repository.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/database/repository/reservation.repository.ts b/src/database/repository/reservation.repository.ts index 4f6e086..0eb7b81 100644 --- a/src/database/repository/reservation.repository.ts +++ b/src/database/repository/reservation.repository.ts @@ -153,7 +153,11 @@ export class ReservationRepository { const reservation = await prisma.reservation.findUnique({ where: { id: reservationId }, }); - if (!reservation || reservation.status !== ReservationStatus.ACCEPT) + if ( + !reservation || + (reservation.status !== ReservationStatus.ACCEPT && + reservation.status !== ReservationStatus.MENTEE_CHECKED) + ) throw new BadRequestException('invalid reservation for mentee_completion'); if (role !== UserRole.ADMIN && reservation.menteeId !== userId) throw new UnauthorizedException('user is not mentee of this reservation'); From 338d68645c506a767c38e2a3ca93f3c897f892ac Mon Sep 17 00:00:00 2001 From: myukang Date: Tue, 5 Sep 2023 15:45:08 +0900 Subject: [PATCH 6/9] :sparkles: [Feature]: check reservation - controller/service/repository --- src/common/dto/param/id.dto.ts | 7 ++++ .../repository/reservation.repository.ts | 40 +++++++++++++++++++ .../reservation/reservation.controller.ts | 14 +++++++ src/models/reservation/reservation.service.ts | 14 +++++++ 4 files changed, 75 insertions(+) create mode 100644 src/common/dto/param/id.dto.ts diff --git a/src/common/dto/param/id.dto.ts b/src/common/dto/param/id.dto.ts new file mode 100644 index 0000000..856b46d --- /dev/null +++ b/src/common/dto/param/id.dto.ts @@ -0,0 +1,7 @@ +// validate-param.dto.ts +import { IsPositive } from 'class-validator'; + +export class IdParamDto { + @IsPositive() + id: number; +} diff --git a/src/database/repository/reservation.repository.ts b/src/database/repository/reservation.repository.ts index 0eb7b81..6234bdd 100644 --- a/src/database/repository/reservation.repository.ts +++ b/src/database/repository/reservation.repository.ts @@ -2,6 +2,7 @@ import { BadRequestException, ConflictException, Injectable, + NotFoundException, UnauthorizedException, } from '@nestjs/common'; import { PrismaService } from '../services/prisma.service'; @@ -143,6 +144,45 @@ export class ReservationRepository { }); }); } + + /** + * @param reservationId + * - 예약 ID + * @param userId + * - 요청한 유저 ID + * @param role + * - 요청한 유저의 Role + * */ + async checkReservation( + reservationId: number, + userId: number, + role: string, + ): Promise { + return this.prismaService.$transaction(async (prisma) => { + const reservation = await this.prismaService.reservation.findUnique({ + where: { + id: reservationId, + }, + }); + // reservation이 없는 경우 + if (!reservation) throw new NotFoundException('not exist reservation'); + + // check는 ACCEPT 상태의 예약만 가능 + if (reservation.status !== ReservationStatus.ACCEPT) + throw new BadRequestException('invalid reservation for check'); + + // check는 예약한 멘티 혹은 Admin 만 가능 + if (reservation.menteeId !== userId && role !== 'ADMIN') + throw new UnauthorizedException('invalid user'); + + return prisma.reservation.update({ + where: { id: reservationId }, + data: { status: ReservationStatus.MENTEE_CHECKED }, + select: ReservationSelectQuery, + }); + }); + } + async completeReservationByMentee( reservationId: number, userId: number, diff --git a/src/models/reservation/reservation.controller.ts b/src/models/reservation/reservation.controller.ts index e1c96b9..e70dc83 100644 --- a/src/models/reservation/reservation.controller.ts +++ b/src/models/reservation/reservation.controller.ts @@ -23,6 +23,7 @@ import { JwtGuard } from '../../common/guards/jwt/jwt.guard'; import { GetUserId } from '../../common/decorators/getUserId.decorator'; import { UserRole } from '@prisma/client'; import { GetUserRole } from '../../common/decorators/getUserRole.decorator'; +import { IdParamDto } from '../../common/dto/param/id.dto'; @Controller('/reservations') export class ReservationController { @@ -125,6 +126,19 @@ export class ReservationController { return await this.reservationService.acceptReservation(reservationId, userId, role); } + /** + * @access >= OWNER + * */ + @Patch('/:id/check') + @UseGuards(JwtGuard) + async check( + @Param('id') param: IdParamDto, + @GetUserId() userId: number, + @GetUserRole() role: UserRole, + ): Promise { + return await this.reservationService.checkReservationByMentee(param.id, userId, role); + } + /** * @access >= OWNER */ diff --git a/src/models/reservation/reservation.service.ts b/src/models/reservation/reservation.service.ts index 7feb615..e5eadb9 100644 --- a/src/models/reservation/reservation.service.ts +++ b/src/models/reservation/reservation.service.ts @@ -100,6 +100,20 @@ export class ReservationService { this.eventEmitter.emit(RESERVATION_ACCEPT, { mentor, mentee, reservation: result }); return result; } + + /** + * @description 멘티가 예약을 확인하는 API + * - 멘티가 멘토에게 예약을 요청한 경우, 멘토가 수락하면 예약이 생성된다. + * - 수락된 예약은 멘티가 확인을 했느냐/하지 않았냐에 따라 달라짐. + * */ + async checkReservationByMentee( + reservationId: number, + userId: number, + role: string, + ): Promise { + return await this.reservationRepository.checkReservation(reservationId, userId, role); + } + async menteeCompletion( reservationId: number, userId: number, From 171de60bc84b150fd7f6e8a8e3d533ff55526e93 Mon Sep 17 00:00:00 2001 From: myukang Date: Wed, 6 Sep 2023 00:02:32 +0900 Subject: [PATCH 7/9] :recycle: [Refactor]: moduleNameMapper --- e2e/jest-e2e.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/e2e/jest-e2e.json b/e2e/jest-e2e.json index e9d912f..54be8a3 100644 --- a/e2e/jest-e2e.json +++ b/e2e/jest-e2e.json @@ -5,5 +5,8 @@ "testRegex": ".e2e-spec.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" + }, + "moduleNameMapper": { + "^src/(.*)$": "/../src/$1" } } From 6394aedaee9eac74ed3441f9914e868bd0de2272 Mon Sep 17 00:00:00 2001 From: myukang Date: Wed, 6 Sep 2023 00:04:09 +0900 Subject: [PATCH 8/9] :white_check_mark: [Test]: e2e test - add e2e test reservation check - IdParamDto remove --- e2e/reservation/reservation.e2e-spec.ts | 28 ++++++++++++++++++- src/common/dto/param/id.dto.ts | 7 ----- .../reservation/reservation.controller.ts | 5 ++-- src/models/reservation/reservation.service.ts | 1 - 4 files changed, 29 insertions(+), 12 deletions(-) delete mode 100644 src/common/dto/param/id.dto.ts diff --git a/e2e/reservation/reservation.e2e-spec.ts b/e2e/reservation/reservation.e2e-spec.ts index bf39cfa..269ba73 100644 --- a/e2e/reservation/reservation.e2e-spec.ts +++ b/e2e/reservation/reservation.e2e-spec.ts @@ -6,7 +6,7 @@ import { ValidationOptions } from '../../src/common/pipes/validationPipe/validat import { PrismaClientExceptionFilter } from '../../src/common/filters/prismaClientException.filter'; import { User } from '@prisma/client'; import { Category, Hashtag, MentorProfile, Reservation } from '.prisma/client'; -import { AppModule } from '../../src/app.module'; +import { AppModule } from 'src/app.module'; /** * @description @@ -255,6 +255,7 @@ describe('Reservation - Request', () => { describe('Request - Accept', () => { let reservation: Reservation; + //Accept된 예약 생성 beforeEach(async () => { const response = await request(app.getHttpServer()) .post('/reservations') @@ -339,6 +340,31 @@ describe('Reservation - Request', () => { expect(res.status).toBe('DONE'); }); }); + + describe('Accept -> Checked By mentee', () => { + it('참여하지 않은 멘티가 예약을 확인한다.(401)', async () => { + const response = await request(app.getHttpServer()) + .patch(`/reservations/${reservation.id}/check`) + .set('Authorization', `Bearer ${dummyMenteeAccToken}`); + + expect(response.status).toBe(401); + }); + + it('멘티가 예약을 확인한다.(200)', async () => { + const response = await request(app.getHttpServer()) + .patch(`/reservations/${reservation.id}/check`) + .set('Authorization', `Bearer ${menteeAccessToken}`); + + expect(response.status).toBe(200); + + const res = await prisma.reservation.findUnique({ + where: { + id: reservation.id, + }, + }); + expect(res.status).toBe('MENTEE_CHECKED'); + }); + }); }); /** diff --git a/src/common/dto/param/id.dto.ts b/src/common/dto/param/id.dto.ts deleted file mode 100644 index 856b46d..0000000 --- a/src/common/dto/param/id.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -// validate-param.dto.ts -import { IsPositive } from 'class-validator'; - -export class IdParamDto { - @IsPositive() - id: number; -} diff --git a/src/models/reservation/reservation.controller.ts b/src/models/reservation/reservation.controller.ts index e70dc83..b419d6e 100644 --- a/src/models/reservation/reservation.controller.ts +++ b/src/models/reservation/reservation.controller.ts @@ -23,7 +23,6 @@ import { JwtGuard } from '../../common/guards/jwt/jwt.guard'; import { GetUserId } from '../../common/decorators/getUserId.decorator'; import { UserRole } from '@prisma/client'; import { GetUserRole } from '../../common/decorators/getUserRole.decorator'; -import { IdParamDto } from '../../common/dto/param/id.dto'; @Controller('/reservations') export class ReservationController { @@ -132,11 +131,11 @@ export class ReservationController { @Patch('/:id/check') @UseGuards(JwtGuard) async check( - @Param('id') param: IdParamDto, + @Param('id') reservationId: number, @GetUserId() userId: number, @GetUserRole() role: UserRole, ): Promise { - return await this.reservationService.checkReservationByMentee(param.id, userId, role); + return await this.reservationService.checkReservationByMentee(reservationId, userId, role); } /** diff --git a/src/models/reservation/reservation.service.ts b/src/models/reservation/reservation.service.ts index e5eadb9..566f4a8 100644 --- a/src/models/reservation/reservation.service.ts +++ b/src/models/reservation/reservation.service.ts @@ -6,7 +6,6 @@ import { ReservationCompleteAsMentorPayloadDto, ReservationUpdatePayloadDto, } from './dto/request/reservationUpdatePayload.dto'; -import { GetReservationQueryDto } from './dto/request/reservationQuery.dto'; import { ReservationRepository } from '../../database/repository/reservation.repository'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { UserRole } from '@prisma/client'; From 733f239ac6de70315f15d180a222ca272a22c86f Mon Sep 17 00:00:00 2001 From: myukang Date: Wed, 6 Sep 2023 00:09:32 +0900 Subject: [PATCH 9/9] :bug: [Fix]: build bot - add npx prisma generate --- .github/workflows/build_test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 708121b..db9820a 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -33,5 +33,7 @@ jobs: - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: npm ci + - name: prisma generate + run: npx prisma generate - name: build run: npm run build \ No newline at end of file