Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/build_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
33 changes: 33 additions & 0 deletions api-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions e2e/jest-e2e.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"moduleNameMapper": {
"^src/(.*)$": "<rootDir>/../src/$1"
}
}
28 changes: 27 additions & 1 deletion e2e/reservation/reservation.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -255,6 +255,7 @@ describe('Reservation - Request', () => {

describe('Request - Accept', () => {
let reservation: Reservation;
//Accept된 예약 생성
beforeEach(async () => {
const response = await request(app.getHttpServer())
.post('/reservations')
Expand Down Expand Up @@ -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');
});
});
});

/**
Expand Down
2 changes: 2 additions & 0 deletions prisma/migrations/20230905050509_mentee_checked/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `reservations` MODIFY `status` ENUM('REQUEST', 'ACCEPT', 'CANCEL', 'MENTEE_CHECKED', 'MENTEE_FEEDBACK', 'DONE') NOT NULL DEFAULT 'REQUEST';
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ enum ReservationStatus {
REQUEST
ACCEPT
CANCEL
MENTEE_CHECKED
MENTEE_FEEDBACK
DONE
}
Expand Down
61 changes: 54 additions & 7 deletions src/database/repository/reservation.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { PrismaService } from '../services/prisma.service';
Expand Down Expand Up @@ -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({
Expand All @@ -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<ReservationGetResponseDto> {
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,
Expand All @@ -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');
Expand Down
13 changes: 13 additions & 0 deletions src/models/reservation/reservation.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReservationGetResponseDto> {
return await this.reservationService.checkReservationByMentee(reservationId, userId, role);
}

/**
* @access >= OWNER
*/
Expand Down
15 changes: 14 additions & 1 deletion src/models/reservation/reservation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<ReservationGetResponseDto> {
return await this.reservationRepository.checkReservation(reservationId, userId, role);
}

async menteeCompletion(
reservationId: number,
userId: number,
Expand Down