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 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 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" } } 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/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 } diff --git a/src/database/repository/reservation.repository.ts b/src/database/repository/reservation.repository.ts index e044163..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'; @@ -99,21 +100,24 @@ 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'); + /** - * 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({ @@ -140,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, @@ -150,7 +193,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'); diff --git a/src/models/reservation/reservation.controller.ts b/src/models/reservation/reservation.controller.ts index e1c96b9..b419d6e 100644 --- a/src/models/reservation/reservation.controller.ts +++ b/src/models/reservation/reservation.controller.ts @@ -125,6 +125,19 @@ export class ReservationController { return await this.reservationService.acceptReservation(reservationId, userId, role); } + /** + * @access >= OWNER + * */ + @Patch('/:id/check') + @UseGuards(JwtGuard) + async check( + @Param('id') reservationId: number, + @GetUserId() userId: number, + @GetUserRole() role: UserRole, + ): Promise { + return await this.reservationService.checkReservationByMentee(reservationId, userId, role); + } + /** * @access >= OWNER */ diff --git a/src/models/reservation/reservation.service.ts b/src/models/reservation/reservation.service.ts index 7feb615..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'; @@ -100,6 +99,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,