From e1a0c59dd2d613c288ddd0ccf01ac5724aa5e809 Mon Sep 17 00:00:00 2001 From: kimsoo0119 Date: Sun, 19 May 2024 17:15:31 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Refactor(#424):=20=EC=8A=A4=EC=9B=A8?= =?UTF-8?q?=EA=B1=B0=20=EC=84=A0=EC=96=B8=EB=B0=A9=EC=8B=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../lecturer-payments.controller.ts | 103 +++++----- .../controllers/payments.controller.ts | 2 - .../swagger/lecturer-payments.swagger.ts | 181 ++++++++++++++++++ .../repository/payments.repository.ts | 1 - .../services/lecturer-payments.service.ts | 5 +- .../ApiCreateLecturePaymentInfo.ts | 53 ----- .../create-lecture-payment-info-decorater.ts | 55 ------ ...ure-payment-info-with-deposit-decorater.ts | 59 ------ ...re-payment-info-with-transfer-decorater.ts | 55 ------ .../create-lecturer-bank-account.decorator.ts | 18 -- .../get-lecturer-payment-list.decorator.ts | 18 -- ...ecturer-payment-request-count.decorator.ts | 14 -- ...-lecturer-recent-bank-account.decorator.ts | 18 -- .../get-my-pass-situation.decorator.ts | 19 -- .../get-payment-request-list.decorator.ts | 19 -- .../get-revenue-statistics.decorator.ts | 19 -- .../get-total-revenue.decorator.ts | 13 -- ...update-payment-request-status.decorator.ts | 49 ----- 18 files changed, 236 insertions(+), 465 deletions(-) create mode 100644 src/payments/controllers/swagger/lecturer-payments.swagger.ts delete mode 100644 src/payments/swagger-decorators/ApiCreateLecturePaymentInfo.ts delete mode 100644 src/payments/swagger-decorators/create-lecture-payment-info-decorater.ts delete mode 100644 src/payments/swagger-decorators/create-lecture-payment-info-with-deposit-decorater.ts delete mode 100644 src/payments/swagger-decorators/create-lecture-payment-info-with-transfer-decorater.ts delete mode 100644 src/payments/swagger-decorators/create-lecturer-bank-account.decorator.ts delete mode 100644 src/payments/swagger-decorators/get-lecturer-payment-list.decorator.ts delete mode 100644 src/payments/swagger-decorators/get-lecturer-payment-request-count.decorator.ts delete mode 100644 src/payments/swagger-decorators/get-lecturer-recent-bank-account.decorator.ts delete mode 100644 src/payments/swagger-decorators/get-my-pass-situation.decorator.ts delete mode 100644 src/payments/swagger-decorators/get-payment-request-list.decorator.ts delete mode 100644 src/payments/swagger-decorators/get-revenue-statistics.decorator.ts delete mode 100644 src/payments/swagger-decorators/get-total-revenue.decorator.ts delete mode 100644 src/payments/swagger-decorators/update-payment-request-status.decorator.ts diff --git a/src/payments/controllers/lecturer-payments.controller.ts b/src/payments/controllers/lecturer-payments.controller.ts index 2f090799..5af20dd5 100644 --- a/src/payments/controllers/lecturer-payments.controller.ts +++ b/src/payments/controllers/lecturer-payments.controller.ts @@ -11,30 +11,22 @@ import { } from '@nestjs/common'; import { LecturerPaymentsService } from '@src/payments/services/lecturer-payments.service'; import { ApiTags } from '@nestjs/swagger'; -import { ApiCreateLecturerBankAccount } from '@src/payments/swagger-decorators/create-lecturer-bank-account.decorator'; import { SetResponseKey } from '@src/common/decorator/set-response-meta-data.decorator'; import { GetAuthorizedUser } from '@src/common/decorator/get-user.decorator'; import { ValidateResult } from '@src/common/interface/common-interface'; import { CreateBankAccountDto } from '@src/payments/dtos/create-bank-account.dto'; import { LecturerBankAccountDto } from '@src/payments/dtos/lecturer-bank-account.dto'; import { LecturerAccessTokenGuard } from '@src/common/guards/lecturer-access-token.guard'; -import { ApiGetLecturerRecentBankAccount } from '@src/payments/swagger-decorators/get-lecturer-recent-bank-account.decorator'; -import { ApiGetPaymentRequestList } from '@src/payments/swagger-decorators/get-payment-request-list.decorator'; import { PaymentRequestDto } from '@src/payments/dtos/payment-request.dto'; -import { UpdatePaymentRequestStatusDto } from '@src/payments/dtos/update-payment-request.dto'; -import { ApiUpdatePaymentRequestStatus } from '@src/payments/swagger-decorators/update-payment-request-status.decorator'; -import { ApiGetPaymentRequestCount } from '@src/payments/swagger-decorators/get-lecturer-payment-request-count.decorator'; import { PassSituationDto } from '@src/payments/dtos/response/pass-situation.dto'; -import { ApiGetMyPassSituation } from '@src/payments/swagger-decorators/get-my-pass-situation.decorator'; import { GetRevenueStatisticsDto } from '../dtos/request/get-revenue-statistics.dto'; -import { ApiGetRevenueStatistics } from '../swagger-decorators/get-revenue-statistics.decorator'; import { plainToInstance } from 'class-transformer'; import { RevenueStatisticDto } from '../dtos/response/revenue-statistic.dto'; import { GetLecturerPaymentListDto } from '../dtos/request/get-lecturer-payment-list.dto'; import { LecturerPaymentItemDto } from '../dtos/response/lecturer-payment-item.dto'; -import { ApiGetLecturerPaymentList } from '../swagger-decorators/get-lecturer-payment-list.decorator'; import { GetTotalRevenueDto } from '../dtos/request/get-total-revenue.dto'; -import { ApiGetTotalRevenue } from '../swagger-decorators/get-total-revenue.decorator'; +import { ApiLecturerPayments } from './swagger/lecturer-payments.swagger'; +import { UpdatePaymentRequestStatusDto } from '../dtos/update-payment-request.dto'; @ApiTags('강사-결제') @UseGuards(LecturerAccessTokenGuard) @@ -44,7 +36,7 @@ export class LecturerPaymentsController { private readonly lecturerPaymentsService: LecturerPaymentsService, ) {} - @ApiGetLecturerPaymentList() + @ApiLecturerPayments.GetLecturerPaymentList({ summary: '판매 내역' }) @Get() async getLecturerPaymentList( @GetAuthorizedUser() authorizedData: ValidateResult, @@ -68,7 +60,7 @@ export class LecturerPaymentsController { }; } - @ApiGetTotalRevenue() + @ApiLecturerPayments.GetTotalRevenue({ summary: '총 매출액' }) @SetResponseKey('totalRevenue') @Get('/total-revenue') async getTotalRevenue( @@ -81,7 +73,7 @@ export class LecturerPaymentsController { ); } - @ApiGetRevenueStatistics() + @ApiLecturerPayments.GetRevenueStatistics({ summary: '매출 통계' }) @SetResponseKey('revenueStatistics') @Get('/revenue-statistics') async getRevenueStatistics( @@ -97,7 +89,9 @@ export class LecturerPaymentsController { return plainToInstance(RevenueStatisticDto, revenueStatistics); } - @ApiGetLecturerRecentBankAccount() + @ApiLecturerPayments.GetUserRecentBankAccount({ + summary: '강사가 최근 등록(사용)한 계좌 조회', + }) @SetResponseKey('lecturerRecentBankAccount') @Get('/recent-bank-account') async getUserRecentBankAccount( @@ -108,7 +102,9 @@ export class LecturerPaymentsController { ); } - @ApiCreateLecturerBankAccount() + @ApiLecturerPayments.CreateLecturerBankAccount({ + summary: '강사 계좌 등록', + }) @SetResponseKey('createdLecturerBankAccount') @Post('/bank-account') async createLecturerBankAccount( @@ -121,41 +117,7 @@ export class LecturerPaymentsController { ); } - @ApiGetPaymentRequestList() - @SetResponseKey('requestList') - @Get('/requests') - async getPaymentRequestList( - @GetAuthorizedUser() authorizedData: ValidateResult, - ): Promise { - return await this.lecturerPaymentsService.getPaymentRequestList( - authorizedData.lecturer.id, - ); - } - - @ApiGetPaymentRequestCount() - @SetResponseKey('requestCount') - @Get('/requests/count') - async getPaymentRequestCount( - @GetAuthorizedUser() authorizedData: ValidateResult, - ): Promise { - return await this.lecturerPaymentsService.getPaymentRequestCount( - authorizedData.lecturer.id, - ); - } - - @ApiUpdatePaymentRequestStatus() - @Patch('/request') - async updatePaymentRequestStatus( - @GetAuthorizedUser() authorizedData: ValidateResult, - @Body() updatePaymentRequestStatusDto: UpdatePaymentRequestStatusDto, - ): Promise { - await this.lecturerPaymentsService.updatePaymentRequestStatus( - authorizedData.lecturer.id, - updatePaymentRequestStatusDto, - ); - } - - @ApiGetMyPassSituation() + @ApiLecturerPayments.GetMyPassSituation({ summary: '패스권 판매 현황' }) @SetResponseKey('passSituationList') @Get('passes/:passId') async getMyPassSituation( @@ -167,4 +129,45 @@ export class LecturerPaymentsController { passId, ); } + + //todo 사용 여부 확이 + // @ApiLecturerPayments.GetPaymentRequestList({ + // summary: '입금 대기 중인 결제내역 조회', + // }) + // @SetResponseKey('requestList') + // @Get('/requests') + // async getPaymentRequestList( + // @GetAuthorizedUser() authorizedData: ValidateResult, + // ): Promise { + // return await this.lecturerPaymentsService.getPaymentRequestList( + // authorizedData.lecturer.id, + // ); + // } + + // @ApiLecturerPayments.GetPaymentRequestCount({ + // summary: '입금 대기 중인 결제 건수 조회', + // }) + // @SetResponseKey('requestCount') + // @Get('/requests/count') + // async getPaymentRequestCount( + // @GetAuthorizedUser() authorizedData: ValidateResult, + // ): Promise { + // return await this.lecturerPaymentsService.getPaymentRequestCount( + // authorizedData.lecturer.id, + // ); + // } + + // @ApiLecturerPayments.UpdatePaymentRequestStatus({ + // summary: '결제 요청 상태 변경', + // }) + // @Patch('/request') + // async updatePaymentRequestStatus( + // @GetAuthorizedUser() authorizedData: ValidateResult, + // @Body() updatePaymentRequestStatusDto: UpdatePaymentRequestStatusDto, + // ): Promise { + // await this.lecturerPaymentsService.updatePaymentRequestStatus( + // authorizedData.lecturer.id, + // updatePaymentRequestStatusDto, + // ); + // } } diff --git a/src/payments/controllers/payments.controller.ts b/src/payments/controllers/payments.controller.ts index fcbde630..125c9055 100644 --- a/src/payments/controllers/payments.controller.ts +++ b/src/payments/controllers/payments.controller.ts @@ -18,7 +18,6 @@ import { ApiTags } from '@nestjs/swagger'; import { UserAccessTokenGuard } from '@src/common/guards/user-access-token.guard'; import { GetAuthorizedUser } from '@src/common/decorator/get-user.decorator'; import { ValidateResult } from '@src/common/interface/common-interface'; -import { ApiCreateLecturePaymentInfo } from '../swagger-decorators/ApiCreateLecturePaymentInfo'; import { ConfirmLecturePaymentDto } from '@src/payments/dtos/confirm-lecture-payment.dto'; import { CreatePassPaymentDto } from '@src/payments/dtos/create-pass-payment.dto'; import { CreateLecturePaymentWithPassDto } from '@src/payments/dtos/create-lecture-payment-with-pass.dto'; @@ -60,7 +59,6 @@ export class PaymentsController { summary: '토스-강의 결제를 위한 기본 결제 정보 생성', }) @SetResponseKey('pendingPaymentInfo') - @ApiCreateLecturePaymentInfo() @Post('/toss/lecture') @UseGuards(UserAccessTokenGuard) createLecturePaymentWithToss( diff --git a/src/payments/controllers/swagger/lecturer-payments.swagger.ts b/src/payments/controllers/swagger/lecturer-payments.swagger.ts new file mode 100644 index 00000000..f977ae79 --- /dev/null +++ b/src/payments/controllers/swagger/lecturer-payments.swagger.ts @@ -0,0 +1,181 @@ +import { ApiOperator } from '@src/common/types/type'; +import { LecturerPaymentsController } from '../lecturer-payments.controller'; +import { OperationObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; +import { HttpStatus, applyDecorators } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; +import { ExceptionResponseDto } from '@src/common/swagger/dtos/exeption-response.dto'; +import { PaginationResponseDto } from '@src/common/swagger/dtos/pagination-response.dto'; +import { LecturerPaymentItemDto } from '@src/payments/dtos/response/lecturer-payment-item.dto'; +import { RevenueStatisticDto } from '@src/payments/dtos/response/revenue-statistic.dto'; +import { LecturerBankAccountDto } from '@src/payments/dtos/lecturer-bank-account.dto'; +import { PaymentRequestDto } from '@src/payments/dtos/payment-request.dto'; +import { PassSituationDto } from '@src/payments/dtos/response/pass-situation.dto'; +import { StatusResponseDto } from '@src/common/swagger/dtos/status-response.dto'; + +export const ApiLecturerPayments: ApiOperator< + keyof LecturerPaymentsController +> = { + GetLecturerPaymentList: ( + apiOperationOptions: Required, 'summary'>> & + Partial, + ): PropertyDecorator => { + return applyDecorators( + ApiOperation(apiOperationOptions), + ApiBearerAuth(), + PaginationResponseDto.swaggerBuilder( + HttpStatus.OK, + 'lecturerPaymentList', + LecturerPaymentItemDto, + ), + ExceptionResponseDto.swaggerBuilder(HttpStatus.NOT_FOUND, [ + { + error: 'PaymentInfoNotFound', + description: '결제 정보가 존재하지 않습니다.', + }, + ]), + ); + }, + + GetTotalRevenue: ( + apiOperationOptions: Required, 'summary'>> & + Partial, + ): PropertyDecorator => { + return applyDecorators( + ApiOperation(apiOperationOptions), + ApiBearerAuth(), + DetailResponseDto.swaggerBuilder(HttpStatus.OK, 'totalRevenue', Number), + ); + }, + + GetRevenueStatistics: ( + apiOperationOptions: Required, 'summary'>> & + Partial, + ): PropertyDecorator => { + return applyDecorators( + ApiOperation(apiOperationOptions), + ApiBearerAuth(), + DetailResponseDto.swaggerBuilder( + HttpStatus.OK, + 'revenueStatistics', + RevenueStatisticDto, + { isArray: true }, + ), + ); + }, + + GetUserRecentBankAccount: ( + apiOperationOptions: Required, 'summary'>> & + Partial, + ): PropertyDecorator => { + return applyDecorators( + ApiOperation(apiOperationOptions), + ApiBearerAuth(), + DetailResponseDto.swaggerBuilder( + HttpStatus.OK, + 'lecturerRecentBankAccount', + LecturerBankAccountDto, + ), + ); + }, + + CreateLecturerBankAccount: ( + apiOperationOptions: Required, 'summary'>> & + Partial, + ): PropertyDecorator => { + return applyDecorators( + ApiOperation(apiOperationOptions), + ApiBearerAuth(), + DetailResponseDto.swaggerBuilder( + HttpStatus.CREATED, + 'createdLecturerBankAccount', + LecturerBankAccountDto, + ), + ); + }, + + GetMyPassSituation: ( + apiOperationOptions: Required, 'summary'>> & + Partial, + ): PropertyDecorator => { + return applyDecorators( + ApiOperation(apiOperationOptions), + ApiBearerAuth(), + DetailResponseDto.swaggerBuilder( + HttpStatus.OK, + 'passSituationList', + PassSituationDto, + { isArray: true }, + ), + ExceptionResponseDto.swaggerBuilder(HttpStatus.NOT_FOUND, [ + { + error: 'PassNotFound', + description: '패스권이 존재하지 않습니다.', + }, + ]), + ); + }, + + // GetPaymentRequestList: ( + // apiOperationOptions: Required, 'summary'>> & + // Partial, + // ): PropertyDecorator => { + // return applyDecorators( + // ApiOperation(apiOperationOptions), + // ApiBearerAuth(), + // DetailResponseDto.swaggerBuilder( + // HttpStatus.OK, + // 'requestList', + // PaymentRequestDto, + // { isArray: true }, + // ), + // ); + // }, + + // GetPaymentRequestCount: ( + // apiOperationOptions: Required, 'summary'>> & + // Partial, + // ): PropertyDecorator => { + // return applyDecorators( + // ApiOperation(apiOperationOptions), + // ApiBearerAuth(), + // DetailResponseDto.swaggerBuilder(HttpStatus.OK, 'requestCount', Number), + // ); + // }, + + // UpdatePaymentRequestStatus: ( + // apiOperationOptions: Required, 'summary'>> & + // Partial, + // ): PropertyDecorator => { + // return applyDecorators( + // ApiOperation(apiOperationOptions), + // ApiBearerAuth(), + // StatusResponseDto.swaggerBuilder( + // HttpStatus.OK, + // 'updatePaymentRequestResult', + // ), + // ExceptionResponseDto.swaggerBuilder(HttpStatus.BAD_REQUEST, [ + // { + // error: 'InvalidPayment', + // description: '잘못된 결제 정보입니다.', + // }, + // { + // error: 'InvalidPaymentMethod', + // description: '해당 결제 정보는 변경이 불가능한 결제 방식입니다.', + // }, + // { + // error: 'PaymentStatusAlreadyUpdated', + // description: '해당 결제 정보는 이미 변경된 상태입니다.', + // }, + // { + // error: 'ExceededMaxParticipants', + // description: '최대 인원 초과로 인해 취소할 수 없습니다.', + // }, + // { + // error: 'InvalidRefundAmount', + // description: '환불금액이 올바르지 않습니다.', + // }, + // ]), + // ); + // }, +}; diff --git a/src/payments/repository/payments.repository.ts b/src/payments/repository/payments.repository.ts index 115b89ff..763a2a01 100644 --- a/src/payments/repository/payments.repository.ts +++ b/src/payments/repository/payments.repository.ts @@ -1021,7 +1021,6 @@ export class PaymentsRepository { }, }); } - async getLecturerLectureList(lecturerId: number): Promise { return await this.prismaService.lecture.findMany({ where: { lecturerId } }); } diff --git a/src/payments/services/lecturer-payments.service.ts b/src/payments/services/lecturer-payments.service.ts index 64a37703..2dac03e7 100644 --- a/src/payments/services/lecturer-payments.service.ts +++ b/src/payments/services/lecturer-payments.service.ts @@ -9,7 +9,6 @@ import { CreateBankAccountDto } from '@src/payments/dtos/create-bank-account.dto import { LecturerBankAccountDto } from '@src/payments/dtos/lecturer-bank-account.dto'; import { PaymentRequestDto } from '@src/payments/dtos/payment-request.dto'; import { Lecture, LecturePass } from '@prisma/client'; -import { UpdatePaymentRequestStatusDto } from '@src/payments/dtos/update-payment-request.dto'; import { LectureMethod, PaymentHistoryTypes, @@ -29,6 +28,7 @@ import { RevenueStatisticDto } from '../dtos/response/revenue-statistic.dto'; import { GetLecturerPaymentListDto } from '../dtos/request/get-lecturer-payment-list.dto'; import { LecturerPaymentItemDto } from '../dtos/response/lecturer-payment-item.dto'; import { GetTotalRevenueDto } from '../dtos/request/get-total-revenue.dto'; +import { UpdatePaymentRequestStatusDto } from '../dtos/update-payment-request.dto'; @Injectable() export class LecturerPaymentsService { @@ -168,7 +168,6 @@ export class LecturerPaymentsService { return selectedPayment; } - private async processPaymentDoneStatus(paymentId: number): Promise { await this.prismaService.$transaction( async (transaction: PrismaTransaction) => { @@ -422,7 +421,7 @@ export class LecturerPaymentsService { ); if (!selectedPass) { - throw new NotFoundException(`패스권이 존재하지 않습니다`); + throw new NotFoundException(`패스권이 존재하지 않습니다`, 'PassNotFound'); } return selectedPass; diff --git a/src/payments/swagger-decorators/ApiCreateLecturePaymentInfo.ts b/src/payments/swagger-decorators/ApiCreateLecturePaymentInfo.ts deleted file mode 100644 index d517bbff..00000000 --- a/src/payments/swagger-decorators/ApiCreateLecturePaymentInfo.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiNotFoundResponse, - ApiOperation, -} from '@nestjs/swagger'; -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { SwaggerApiResponse } from '@src/common/swagger/swagger-response'; -import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; -import { PendingPaymentInfoDto } from '@src/payments/dtos/pending-payment-info.dto'; - -export function ApiCreateLecturePaymentInfo() { - return applyDecorators( - ApiOperation({ - summary: '결제 정보 생성', - description: '반환받은 결제 정보를 통해 토스에게 요청', - }), - ApiBearerAuth(), - DetailResponseDto.swaggerBuilder( - HttpStatus.CREATED, - 'pendingPaymentInfo', - PendingPaymentInfoDto, - ), - ApiBadRequestResponse( - SwaggerApiResponse.exception([ - { - name: 'DuplicateOrderId', - example: { message: '주문Id가 중복되었습니다.' }, - }, - { - name: 'ProductPriceMismatch', - example: { message: '상품 가격이 일치하지 않습니다.' }, - }, - { - name: 'DuplicateDiscount', - example: { message: '할인율은 중복적용이 불가능합니다.' }, - }, - { - name: 'PaymentAlreadyExists', - example: { message: '결제정보가 이미 존재합니다.' }, - }, - ]), - ), - ApiNotFoundResponse( - SwaggerApiResponse.exception([ - { - name: 'NoAvailableCouponsError', - example: { message: '사용가능한 중복 쿠폰이 존재하지 않습니다.' }, - }, - ]), - ), - ); -} diff --git a/src/payments/swagger-decorators/create-lecture-payment-info-decorater.ts b/src/payments/swagger-decorators/create-lecture-payment-info-decorater.ts deleted file mode 100644 index 822c4754..00000000 --- a/src/payments/swagger-decorators/create-lecture-payment-info-decorater.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiCreatedResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, -} from '@nestjs/swagger'; -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { SwaggerApiResponse } from '@src/common/swagger/swagger-response'; -import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; -import { PendingPaymentInfoDto } from '../dtos/pending-payment-info.dto'; - -export function ApiCreateLecturePaymentInfo() { - return applyDecorators( - ApiOperation({ - summary: '결제 정보 생성', - description: '반환받은 결제 정보를 통해 토스에게 요청', - }), - ApiBearerAuth(), - DetailResponseDto.swaggerBuilder( - HttpStatus.CREATED, - 'pendingPaymentInfo', - PendingPaymentInfoDto, - ), - ApiBadRequestResponse( - SwaggerApiResponse.exception([ - { - name: 'DuplicateOrderId', - example: { message: '주문Id가 중복되었습니다.' }, - }, - { - name: 'ProductPriceMismatch', - example: { message: '상품 가격이 일치하지 않습니다.' }, - }, - { - name: 'DuplicateDiscount', - example: { message: '할인율은 중복적용이 불가능합니다.' }, - }, - { - name: 'PaymentAlreadyExists', - example: { message: '결제정보가 이미 존재합니다.' }, - }, - ]), - ), - ApiNotFoundResponse( - SwaggerApiResponse.exception([ - { - name: 'NoAvailableCouponsError', - example: { message: '사용가능한 중복 쿠폰이 존재하지 않습니다.' }, - }, - ]), - ), - ); -} diff --git a/src/payments/swagger-decorators/create-lecture-payment-info-with-deposit-decorater.ts b/src/payments/swagger-decorators/create-lecture-payment-info-with-deposit-decorater.ts deleted file mode 100644 index ebdea165..00000000 --- a/src/payments/swagger-decorators/create-lecture-payment-info-with-deposit-decorater.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiCreatedResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, -} from '@nestjs/swagger'; -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { SwaggerApiResponse } from '@src/common/swagger/swagger-response'; -import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; -import { LegacyPaymentDto } from '../dtos/legacy-payment.dto'; - -export function ApiCreateLecturePaymentWithDeposit() { - return applyDecorators( - ApiOperation({ - summary: '일반결제(현장결제)', - description: '보증금 결제 차액 현장 결제', - }), - ApiBearerAuth(), - DetailResponseDto.swaggerBuilder( - HttpStatus.CREATED, - 'depositPaymentResult', - LegacyPaymentDto, - ), - ApiBadRequestResponse( - SwaggerApiResponse.exception([ - { - name: 'DuplicateOrderId', - example: { message: '주문Id가 중복되었습니다.' }, - }, - { - name: 'ProductPriceMismatch', - example: { message: '상품 가격이 일치하지 않습니다.' }, - }, - { - name: 'DepositMissing', - example: { message: '보증금 정보가 누락되었습니다.' }, - }, - { - name: 'DepositMismatch', - example: { message: '보증금 가격이 일치하지 않습니다.' }, - }, - { - name: 'PaymentAlreadyExists', - example: { message: '결제정보가 이미 존재합니다.' }, - }, - ]), - ), - ApiNotFoundResponse( - SwaggerApiResponse.exception([ - { - name: 'NoAvailableCouponsError', - example: { message: '사용가능한 중복 쿠폰이 존재하지 않습니다.' }, - }, - ]), - ), - ); -} diff --git a/src/payments/swagger-decorators/create-lecture-payment-info-with-transfer-decorater.ts b/src/payments/swagger-decorators/create-lecture-payment-info-with-transfer-decorater.ts deleted file mode 100644 index 069cc8c2..00000000 --- a/src/payments/swagger-decorators/create-lecture-payment-info-with-transfer-decorater.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiCreatedResponse, - ApiNotFoundResponse, - ApiOkResponse, - ApiOperation, -} from '@nestjs/swagger'; -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { SwaggerApiResponse } from '@src/common/swagger/swagger-response'; -import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; -import { LegacyPaymentDto } from '../dtos/legacy-payment.dto'; - -export function ApiCreateLecturePaymentWithTransfer() { - return applyDecorators( - ApiOperation({ - summary: '일반결제(선결제)', - description: '전체 금액 계좌이체로 결제(쿠폰 사용 가능)', - }), - ApiBearerAuth(), - DetailResponseDto.swaggerBuilder( - HttpStatus.CREATED, - 'transferPaymentResult', - LegacyPaymentDto, - ), - ApiBadRequestResponse( - SwaggerApiResponse.exception([ - { - name: 'DuplicateOrderId', - example: { message: '주문Id가 중복되었습니다.' }, - }, - { - name: 'ProductPriceMismatch', - example: { message: '상품 가격이 일치하지 않습니다.' }, - }, - { - name: 'DuplicateDiscount', - example: { message: '할인율은 중복적용이 불가능합니다.' }, - }, - { - name: 'PaymentAlreadyExists', - example: { message: '결제정보가 이미 존재합니다.' }, - }, - ]), - ), - ApiNotFoundResponse( - SwaggerApiResponse.exception([ - { - name: 'NoAvailableCouponsError', - example: { message: '사용가능한 중복 쿠폰이 존재하지 않습니다.' }, - }, - ]), - ), - ); -} diff --git a/src/payments/swagger-decorators/create-lecturer-bank-account.decorator.ts b/src/payments/swagger-decorators/create-lecturer-bank-account.decorator.ts deleted file mode 100644 index 1de8e0b3..00000000 --- a/src/payments/swagger-decorators/create-lecturer-bank-account.decorator.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; -import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; -import { LecturerBankAccountDto } from '../dtos/lecturer-bank-account.dto'; - -export function ApiCreateLecturerBankAccount() { - return applyDecorators( - ApiOperation({ - summary: '강사 계좌 등록', - }), - ApiBearerAuth(), - DetailResponseDto.swaggerBuilder( - HttpStatus.CREATED, - 'createdLecturerBankAccount', - LecturerBankAccountDto, - ), - ); -} diff --git a/src/payments/swagger-decorators/get-lecturer-payment-list.decorator.ts b/src/payments/swagger-decorators/get-lecturer-payment-list.decorator.ts deleted file mode 100644 index 63b74e61..00000000 --- a/src/payments/swagger-decorators/get-lecturer-payment-list.decorator.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; -import { PaginationResponseDto } from '@src/common/swagger/dtos/pagination-response.dto'; -import { LecturerPaymentItemDto } from '../dtos/response/lecturer-payment-item.dto'; - -export function ApiGetLecturerPaymentList() { - return applyDecorators( - ApiOperation({ - summary: '판매 내역', - }), - ApiBearerAuth(), - PaginationResponseDto.swaggerBuilder( - HttpStatus.OK, - 'lecturerPaymentList', - LecturerPaymentItemDto, - ), - ); -} diff --git a/src/payments/swagger-decorators/get-lecturer-payment-request-count.decorator.ts b/src/payments/swagger-decorators/get-lecturer-payment-request-count.decorator.ts deleted file mode 100644 index f1af67a2..00000000 --- a/src/payments/swagger-decorators/get-lecturer-payment-request-count.decorator.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; -import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; -import { GeneralResponseDto } from '@src/common/swagger/dtos/general-response.dto'; - -export function ApiGetPaymentRequestCount() { - return applyDecorators( - ApiOperation({ - summary: '승인 대기중인 요청 개수 조회', - }), - ApiBearerAuth(), - DetailResponseDto.swaggerBuilder(HttpStatus.OK, 'requestCount', Number), - ); -} diff --git a/src/payments/swagger-decorators/get-lecturer-recent-bank-account.decorator.ts b/src/payments/swagger-decorators/get-lecturer-recent-bank-account.decorator.ts deleted file mode 100644 index e766fee3..00000000 --- a/src/payments/swagger-decorators/get-lecturer-recent-bank-account.decorator.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; -import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; -import { LecturerBankAccountDto } from '@src/payments/dtos/lecturer-bank-account.dto'; - -export function ApiGetLecturerRecentBankAccount() { - return applyDecorators( - ApiOperation({ - summary: '강사가 최근 등록(사용)한 계좌 조회', - }), - ApiBearerAuth(), - DetailResponseDto.swaggerBuilder( - HttpStatus.OK, - 'lecturerRecentBankAccount', - LecturerBankAccountDto, - ), - ); -} diff --git a/src/payments/swagger-decorators/get-my-pass-situation.decorator.ts b/src/payments/swagger-decorators/get-my-pass-situation.decorator.ts deleted file mode 100644 index 1bf06be7..00000000 --- a/src/payments/swagger-decorators/get-my-pass-situation.decorator.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; -import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; -import { PassSituationDto } from '@src/payments/dtos/response/pass-situation.dto'; - -export function ApiGetMyPassSituation() { - return applyDecorators( - ApiOperation({ - summary: '패스권 판매 현황', - }), - ApiBearerAuth(), - DetailResponseDto.swaggerBuilder( - HttpStatus.OK, - 'passSituationList', - PassSituationDto, - { isArray: true }, - ), - ); -} diff --git a/src/payments/swagger-decorators/get-payment-request-list.decorator.ts b/src/payments/swagger-decorators/get-payment-request-list.decorator.ts deleted file mode 100644 index 2e683088..00000000 --- a/src/payments/swagger-decorators/get-payment-request-list.decorator.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; -import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; -import { PaymentRequestDto } from '../dtos/payment-request.dto'; - -export function ApiGetPaymentRequestList() { - return applyDecorators( - ApiOperation({ - summary: '입금 확인이 필요한 요청 목록 조회', - }), - ApiBearerAuth(), - DetailResponseDto.swaggerBuilder( - HttpStatus.OK, - 'requestList', - PaymentRequestDto, - { isArray: true }, - ), - ); -} diff --git a/src/payments/swagger-decorators/get-revenue-statistics.decorator.ts b/src/payments/swagger-decorators/get-revenue-statistics.decorator.ts deleted file mode 100644 index fd58b005..00000000 --- a/src/payments/swagger-decorators/get-revenue-statistics.decorator.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; -import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; -import { RevenueStatisticDto } from '../dtos/response/revenue-statistic.dto'; - -export function ApiGetRevenueStatistics() { - return applyDecorators( - ApiOperation({ - summary: '수익 통계', - }), - ApiBearerAuth(), - DetailResponseDto.swaggerBuilder( - HttpStatus.OK, - 'revenueStatistics', - RevenueStatisticDto, - { isArray: true }, - ), - ); -} diff --git a/src/payments/swagger-decorators/get-total-revenue.decorator.ts b/src/payments/swagger-decorators/get-total-revenue.decorator.ts deleted file mode 100644 index 1e3dc393..00000000 --- a/src/payments/swagger-decorators/get-total-revenue.decorator.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; -import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; - -export function ApiGetTotalRevenue() { - return applyDecorators( - ApiOperation({ - summary: '총 수익', - }), - ApiBearerAuth(), - DetailResponseDto.swaggerBuilder(HttpStatus.OK, 'totalRevenue', Number), - ); -} diff --git a/src/payments/swagger-decorators/update-payment-request-status.decorator.ts b/src/payments/swagger-decorators/update-payment-request-status.decorator.ts deleted file mode 100644 index ff253217..00000000 --- a/src/payments/swagger-decorators/update-payment-request-status.decorator.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { HttpStatus, applyDecorators } from '@nestjs/common'; -import { - ApiBadRequestResponse, - ApiBearerAuth, - ApiOperation, -} from '@nestjs/swagger'; -import { DetailResponseDto } from '@src/common/swagger/dtos/detail-response-dto'; -import { StatusResponseDto } from '@src/common/swagger/dtos/status-response.dto'; -import { SwaggerApiResponse } from '@src/common/swagger/swagger-response'; -import { UserBankAccountDto } from '@src/payments/dtos/user-bank-account.dto'; - -export function ApiUpdatePaymentRequestStatus() { - return applyDecorators( - ApiOperation({ - summary: '입금 확인이 필요한 요청 상태 변경', - }), - ApiBearerAuth(), - StatusResponseDto.swaggerBuilder( - HttpStatus.OK, - 'updatePaymentRequestResult', - ), - ApiBadRequestResponse( - SwaggerApiResponse.exception([ - { - name: 'InvalidPayment', - example: { message: '잘못된 결제 정보입니다.' }, - }, - { - name: 'InvalidPaymentMethod', - example: { - message: '해당 결제 정보는 변경이 불가능한 결제 방식입니다.', - }, - }, - { - name: 'PaymentStatusAlreadyUpdated', - example: { message: '해당 결제 정보는 이미 변경된 상태입니다.' }, - }, - { - name: 'ExceededMaxParticipants', - example: { message: '최대 인원 초과로 인해 취소할 수 없습니다.' }, - }, - { - name: 'InvalidRefundAmount', - example: { message: '환불금액이 올바르지 않습니다.' }, - }, - ]), - ), - ); -} From 7e9de3c9c9a14dbab90de5afb217f372c5a98ccc Mon Sep 17 00:00:00 2001 From: kimsoo0119 Date: Sun, 19 May 2024 21:22:45 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Incomplete(#424):=20=EA=B0=95=EC=82=AC=20-?= =?UTF-8?q?=20=EA=B2=B0=EC=A0=9C=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 10 +- src/common/types/type.ts | 6 + src/common/utils/date.utils.ts | 40 +++++++ .../lecturer-payments.controller.ts | 6 +- .../services/lecturer-payments.service.ts | 107 ++++++------------ test/unit/utils/dateUtils.spec.ts | 63 +++++++++++ 6 files changed, 154 insertions(+), 78 deletions(-) create mode 100644 src/common/utils/date.utils.ts create mode 100644 test/unit/utils/dateUtils.spec.ts diff --git a/package.json b/package.json index 5ee6058e..a72674c8 100644 --- a/package.json +++ b/package.json @@ -98,8 +98,8 @@ "json", "ts" ], - "rootDir": "src", - "testRegex": ".*\\.spec\\.ts$", + "rootDir": ".", + "testRegex": "^.*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" }, @@ -107,6 +107,10 @@ "**/*.(t|j)s" ], "coverageDirectory": "../coverage", - "testEnvironment": "node" + "testEnvironment": "node", + "moduleNameMapper": { + "^@src/(.*)$": "/src/$1", + "^src/(.*)$": "/src/$1" + } } } diff --git a/src/common/types/type.ts b/src/common/types/type.ts index d7334116..622e3097 100644 --- a/src/common/types/type.ts +++ b/src/common/types/type.ts @@ -6,3 +6,9 @@ export type ApiOperator = { ApiOperationOptions, ) => PropertyDecorator; }; + +export type PaginatedResponse = { + [key in K]: T[]; +} & { + totalItemCount: number; +}; diff --git a/src/common/utils/date.utils.ts b/src/common/utils/date.utils.ts new file mode 100644 index 00000000..2cfb9978 --- /dev/null +++ b/src/common/utils/date.utils.ts @@ -0,0 +1,40 @@ +export class DateUtils { + /** + * 시작 시간과 종료 시간을 반환. + * 빈 값을 넣으면 현재 날짜를 기준으로 00시, 23:59:59:999반환 + * @param startDate 시작 시간 + * @param endDate 종료 시간 + * @returns 시작 시간, 종료 시간 + */ + static getUTCStartAndEndOfRange( + startDate?: Date, + endDate?: Date, + ): { startOfDay: Date; endOfDay: Date } { + const startOfDay = startDate || new Date(); + startOfDay.setUTCHours(0, 0, 0, 0); // UTC 기준 시작 시간 설정 + + const endOfDay = endDate || new Date(); + endOfDay.setUTCHours(23, 59, 59, 999); // UTC 기준 종료 시간 설정 + + return { startOfDay, endOfDay }; + } + + /** + * 특정 달의 시작 시간과 종료 시간을 반환. + * @param year 연도 + * @param month 월 (1월은 0, 12월은 11) + * @returns 시작 시간, 종료 시간 + */ + static getUTCStartAndEndOfMonth( + year: number, + month: number, + ): { startOfMonth: Date; endOfMonth: Date } { + const startOfMonth = new Date(Date.UTC(year, month, 1)); + startOfMonth.setUTCHours(0, 0, 0, 0); // UTC 기준 시작 시간 설정 + + const endOfMonth = new Date(Date.UTC(year, month + 1, 0)); + endOfMonth.setUTCHours(23, 59, 59, 999); // UTC 기준 종료 시간 설정 + + return { startOfMonth, endOfMonth }; + } +} diff --git a/src/payments/controllers/lecturer-payments.controller.ts b/src/payments/controllers/lecturer-payments.controller.ts index 5af20dd5..5e4af339 100644 --- a/src/payments/controllers/lecturer-payments.controller.ts +++ b/src/payments/controllers/lecturer-payments.controller.ts @@ -27,6 +27,7 @@ import { LecturerPaymentItemDto } from '../dtos/response/lecturer-payment-item.d import { GetTotalRevenueDto } from '../dtos/request/get-total-revenue.dto'; import { ApiLecturerPayments } from './swagger/lecturer-payments.swagger'; import { UpdatePaymentRequestStatusDto } from '../dtos/update-payment-request.dto'; +import { PaginatedResponse } from '@src/common/types/type'; @ApiTags('강사-결제') @UseGuards(LecturerAccessTokenGuard) @@ -41,10 +42,7 @@ export class LecturerPaymentsController { async getLecturerPaymentList( @GetAuthorizedUser() authorizedData: ValidateResult, @Query() getLecturerPaymentListDto: GetLecturerPaymentListDto, - ): Promise<{ - totalItemCount: Number; - lecturerPaymentList: LecturerPaymentItemDto[]; - }> { + ): Promise> { const { totalItemCount, lecturerPaymentList } = await this.lecturerPaymentsService.getLecturerPaymentList( authorizedData.lecturer.id, diff --git a/src/payments/services/lecturer-payments.service.ts b/src/payments/services/lecturer-payments.service.ts index 2dac03e7..5f1b177a 100644 --- a/src/payments/services/lecturer-payments.service.ts +++ b/src/payments/services/lecturer-payments.service.ts @@ -29,6 +29,9 @@ import { GetLecturerPaymentListDto } from '../dtos/request/get-lecturer-payment- import { LecturerPaymentItemDto } from '../dtos/response/lecturer-payment-item.dto'; import { GetTotalRevenueDto } from '../dtos/request/get-total-revenue.dto'; import { UpdatePaymentRequestStatusDto } from '../dtos/update-payment-request.dto'; +import { generatePaginationParams } from '@src/common/utils/generate-pagination-params'; +import { DateUtils } from '@src/common/utils/date.utils'; +import { PaginatedResponse } from '@src/common/types/type'; @Injectable() export class LecturerPaymentsService { @@ -525,57 +528,39 @@ export class LecturerPaymentsService { async getLecturerPaymentList( lecturerId: number, dto: GetLecturerPaymentListDto, - ): Promise<{ - totalItemCount: Number; - lecturerPaymentList?: LecturerPaymentItemDto[]; - }> { - const { - currentPage, - targetPage, - firstItemId, - lastItemId, - take, - productType, - startDate, - endDate, - lectureId, - } = dto; - - const paymentProductTypeId = - productType === PaymentHistoryTypes.전체 ? undefined : productType; - - const convertedStartDate = new Date(startDate); - const convertedEndDate = new Date(endDate); - convertedStartDate.setHours(9, 0, 0); - convertedEndDate.setHours(32, 59, 59); - - const paginationParams: IPaginationParams = this.getPaginationParams( - currentPage, - targetPage, - firstItemId, - lastItemId, - take, + ): Promise> { + const { productType, startDate, endDate, lectureId, ...paginationOptions } = + dto; + + const paymentProductTypeId = this.getPaymentTypeId(productType); + + const { startOfDay, endOfDay } = DateUtils.getUTCStartAndEndOfRange( + new Date(startDate), + new Date(endDate), ); + const paginationParams: IPaginationParams = + generatePaginationParams(paginationOptions); + const totalItemCount = await this.paymentsRepository.getLecturerPaymentCount( lecturerId, paymentProductTypeId, - convertedStartDate, - convertedEndDate, + startOfDay, + endOfDay, lectureId, ); - if (!totalItemCount) { - return { totalItemCount }; + if (totalItemCount === 0) { + return { totalItemCount, lecturerPaymentList: [] }; } const lecturerPaymentList = await this.paymentsRepository.getLecturerPaymentList( lecturerId, paymentProductTypeId, - convertedStartDate, - convertedEndDate, + startOfDay, + endOfDay, paginationParams, lectureId, ); @@ -583,53 +568,33 @@ export class LecturerPaymentsService { return { totalItemCount, lecturerPaymentList }; } - private getPaginationParams( - currentPage: number, - targetPage: number, - firstItemId: number, - lastItemId: number, - take: number, - ): IPaginationParams { - let cursor; - let skip; - let updatedTake = take; - - const isPagination = currentPage && targetPage; - const isInfiniteScroll = lastItemId && take; - - if (isPagination) { - const pageDiff = currentPage - targetPage; - cursor = { id: pageDiff <= -1 ? lastItemId : firstItemId }; - skip = Math.abs(pageDiff) === 1 ? 1 : (Math.abs(pageDiff) - 1) * take + 1; - updatedTake = pageDiff >= 1 ? -take : take; - } else if (isInfiniteScroll) { - cursor = { id: lastItemId }; - skip = 1; - } - - return { cursor, skip, take: updatedTake }; - } - async getTotalRevenue( lecturerId: number, dto: GetTotalRevenueDto, ): Promise { const { productType, startDate, endDate, lectureId } = dto; - const paymentProductTypeId = - productType === PaymentHistoryTypes.전체 ? undefined : productType; + const paymentProductTypeId = this.getPaymentTypeId(productType); - const convertedStartDate = new Date(startDate); - const convertedEndDate = new Date(endDate); - convertedStartDate.setHours(9, 0, 0); - convertedEndDate.setHours(32, 59, 59); + const { startOfDay, endOfDay } = DateUtils.getUTCStartAndEndOfRange( + new Date(startDate), + new Date(endDate), + ); return await this.paymentsRepository.getLecturerPaymentTotalRevenue( lecturerId, paymentProductTypeId, - convertedStartDate, - convertedEndDate, + startOfDay, + endOfDay, lectureId, ); } + + private getPaymentTypeId( + paymentHistoryType: PaymentHistoryTypes, + ): number | undefined { + return paymentHistoryType === PaymentHistoryTypes.전체 + ? undefined + : paymentHistoryType; + } } diff --git a/test/unit/utils/dateUtils.spec.ts b/test/unit/utils/dateUtils.spec.ts new file mode 100644 index 00000000..44cde38c --- /dev/null +++ b/test/unit/utils/dateUtils.spec.ts @@ -0,0 +1,63 @@ +import { DateUtils } from '@src/common/utils/date.utils'; + +describe('DateUtils', () => { + describe('getUTCStartAndEndOfRange', () => { + it('날짜가 제공되지 않으면 현재 날짜의 시작과 끝을 반환해야 한다', () => { + const { startOfDay, endOfDay } = DateUtils.getUTCStartAndEndOfRange(); + const now = new Date(); + + expect(startOfDay.getUTCFullYear()).toBe(now.getUTCFullYear()); + expect(startOfDay.getUTCMonth()).toBe(now.getUTCMonth()); + expect(startOfDay.getUTCDate()).toBe(now.getUTCDate()); + expect(startOfDay.getUTCHours()).toBe(0); + expect(startOfDay.getUTCMinutes()).toBe(0); + expect(startOfDay.getUTCSeconds()).toBe(0); + expect(startOfDay.getUTCMilliseconds()).toBe(0); + + expect(endOfDay.getUTCFullYear()).toBe(now.getUTCFullYear()); + expect(endOfDay.getUTCMonth()).toBe(now.getUTCMonth()); + expect(endOfDay.getUTCDate()).toBe(now.getUTCDate()); + expect(endOfDay.getUTCHours()).toBe(23); + expect(endOfDay.getUTCMinutes()).toBe(59); + expect(endOfDay.getUTCSeconds()).toBe(59); + expect(endOfDay.getUTCMilliseconds()).toBe(999); + }); + + it('제공된 날짜의 시작과 끝을 반환해야 한다', () => { + const startDate = new Date(Date.UTC(2023, 0, 1)); + const endDate = new Date(Date.UTC(2023, 0, 1)); + const { startOfDay, endOfDay } = DateUtils.getUTCStartAndEndOfRange( + startDate, + endDate, + ); + + expect(startOfDay.getTime()).toBe(startDate.setUTCHours(0, 0, 0, 0)); + expect(endOfDay.getTime()).toBe(endDate.setUTCHours(23, 59, 59, 999)); + }); + }); + + describe('getUTCStartAndEndOfMonth', () => { + it('주어진 달의 시작과 끝을 반환해야 한다', () => { + const { startOfMonth, endOfMonth } = DateUtils.getUTCStartAndEndOfMonth( + 2023, + 0, + ); + + expect(startOfMonth.getUTCFullYear()).toBe(2023); + expect(startOfMonth.getUTCMonth()).toBe(0); + expect(startOfMonth.getUTCDate()).toBe(1); + expect(startOfMonth.getUTCHours()).toBe(0); + expect(startOfMonth.getUTCMinutes()).toBe(0); + expect(startOfMonth.getUTCSeconds()).toBe(0); + expect(startOfMonth.getUTCMilliseconds()).toBe(0); + + expect(endOfMonth.getUTCFullYear()).toBe(2023); + expect(endOfMonth.getUTCMonth()).toBe(0); + expect(endOfMonth.getUTCDate()).toBe(31); + expect(endOfMonth.getUTCHours()).toBe(23); + expect(endOfMonth.getUTCMinutes()).toBe(59); + expect(endOfMonth.getUTCSeconds()).toBe(59); + expect(endOfMonth.getUTCMilliseconds()).toBe(999); + }); + }); +}); From 7fcedd440cc5d61a17f85cf962e1850ad0a86190 Mon Sep 17 00:00:00 2001 From: kimsoo0119 Date: Mon, 20 May 2024 20:09:46 +0900 Subject: [PATCH 3/3] =?UTF-8?q?Refactor(#424):=20=EA=B0=95=EC=82=AC=20-=20?= =?UTF-8?q?=EA=B2=B0=EC=A0=9C=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EC=99=84=EB=A3=8C,=20DateUtils=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 6 +- src/common/interface/common-interface.ts | 5 + src/common/utils/date.utils.ts | 33 +- .../repository/payments.repository.ts | 167 ++--- .../services/lecturer-payments.service.ts | 694 +++++++++--------- test/unit/utils/dateUtils.spec.ts | 79 +- 6 files changed, 487 insertions(+), 497 deletions(-) diff --git a/package.json b/package.json index a72674c8..87bf2593 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "json", "ts" ], - "rootDir": ".", + "rootDir": "./test", "testRegex": "^.*\\.spec\\.ts$", "transform": { "^.+\\.(t|j)s$": "ts-jest" @@ -109,8 +109,8 @@ "coverageDirectory": "../coverage", "testEnvironment": "node", "moduleNameMapper": { - "^@src/(.*)$": "/src/$1", - "^src/(.*)$": "/src/$1" + "^@src/(.*)$": "/../src/$1", + "^src/(.*)$": "/../src/$1" } } } diff --git a/src/common/interface/common-interface.ts b/src/common/interface/common-interface.ts index a02ee4e9..0902351b 100644 --- a/src/common/interface/common-interface.ts +++ b/src/common/interface/common-interface.ts @@ -98,3 +98,8 @@ export interface IPushNotificationMessage { data: { title: string; body: string; chatRoomId?: string }; token: string; } + +export interface IConvertedDate { + convertedStartDate: Date; + convertedEndDate: Date; +} diff --git a/src/common/utils/date.utils.ts b/src/common/utils/date.utils.ts index 2cfb9978..aabc933f 100644 --- a/src/common/utils/date.utils.ts +++ b/src/common/utils/date.utils.ts @@ -1,3 +1,5 @@ +import { IConvertedDate } from '../interface/common-interface'; + export class DateUtils { /** * 시작 시간과 종료 시간을 반환. @@ -9,32 +11,31 @@ export class DateUtils { static getUTCStartAndEndOfRange( startDate?: Date, endDate?: Date, - ): { startOfDay: Date; endOfDay: Date } { - const startOfDay = startDate || new Date(); - startOfDay.setUTCHours(0, 0, 0, 0); // UTC 기준 시작 시간 설정 + ): IConvertedDate { + const convertedStartDate = startDate ? new Date(startDate) : new Date(); + convertedStartDate.setUTCHours(0, 0, 0, 0); // UTC 기준 시작 시간 설정 - const endOfDay = endDate || new Date(); - endOfDay.setUTCHours(23, 59, 59, 999); // UTC 기준 종료 시간 설정 + const convertedEndDate = endDate + ? new Date(endDate) + : new Date(convertedStartDate); + convertedEndDate.setUTCHours(23, 59, 59, 999); // UTC 기준 종료 시간 설정 - return { startOfDay, endOfDay }; + return { convertedStartDate, convertedEndDate }; } /** - * 특정 달의 시작 시간과 종료 시간을 반환. + * 특정 달의 1일과 마지막 일을 반환. * @param year 연도 * @param month 월 (1월은 0, 12월은 11) * @returns 시작 시간, 종료 시간 */ - static getUTCStartAndEndOfMonth( - year: number, - month: number, - ): { startOfMonth: Date; endOfMonth: Date } { - const startOfMonth = new Date(Date.UTC(year, month, 1)); - startOfMonth.setUTCHours(0, 0, 0, 0); // UTC 기준 시작 시간 설정 + static getUTCStartAndEndOfMonth(year: number, month: number): IConvertedDate { + const convertedStartDate = new Date(Date.UTC(year, month, 1)); + convertedStartDate.setUTCHours(0, 0, 0, 0); // UTC 기준 시작 시간 설정 - const endOfMonth = new Date(Date.UTC(year, month + 1, 0)); - endOfMonth.setUTCHours(23, 59, 59, 999); // UTC 기준 종료 시간 설정 + const convertedEndDate = new Date(Date.UTC(year, month + 1, 0)); + convertedEndDate.setUTCHours(23, 59, 59, 999); // UTC 기준 종료 시간 설정 - return { startOfMonth, endOfMonth }; + return { convertedStartDate, convertedEndDate }; } } diff --git a/src/payments/repository/payments.repository.ts b/src/payments/repository/payments.repository.ts index 763a2a01..cf9f8035 100644 --- a/src/payments/repository/payments.repository.ts +++ b/src/payments/repository/payments.repository.ts @@ -1021,77 +1021,6 @@ export class PaymentsRepository { }, }); } - async getLecturerLectureList(lecturerId: number): Promise { - return await this.prismaService.lecture.findMany({ where: { lecturerId } }); - } - - async getPaymentRequestListByLecturerId(lectureId: number) { - return await this.prismaService.payment.findMany({ - where: { - statusId: PaymentOrderStatus.WAITING_FOR_DEPOSIT, - paymentMethodId: { - in: [PaymentMethods.현장결제, PaymentMethods.선결제], - }, - OR: [ - { - reservation: { - lectureSchedule: { lectureId }, - }, - }, - { - reservation: { - regularLectureStatus: { lectureId }, - }, - }, - ], - }, - include: { - user: { include: { userProfileImage: true } }, - paymentProductType: true, - paymentStatus: true, - paymentMethod: true, - paymentCouponUsage: true, - transferPaymentInfo: { include: { lecturerBankAccount: true } }, - refundPaymentInfo: { - include: { refundStatus: true, refundUserBankAccount: true }, - }, - reservation: { - include: { - lectureSchedule: true, - regularLectureStatus: { include: { regularLectureSchedule: true } }, - }, - }, - cardPaymentInfo: { include: { issuer: true, acquirer: true } }, - virtualAccountPaymentInfo: { include: { bank: true } }, - paymentPassUsage: { - include: { lecturePass: true }, - }, - userPass: { include: { lecturePass: true } }, - }, - orderBy: { - id: 'asc', - }, - }); - } - - async getPaymentRequest( - paymentId: number, - lecturerId: number, - ): Promise { - return this.prismaService.payment.findFirst({ - where: { - id: paymentId, - lecturerId, - paymentProductType: { name: PaymentProductTypes.클래스 }, - }, - include: { - transferPaymentInfo: true, - reservation: { - include: { lectureSchedule: true, regularLectureStatus: true }, - }, - }, - }); - } async getTransferPayment(paymentId: number) { return this.prismaService.transferPaymentInfo.findUnique({ @@ -1128,18 +1057,6 @@ export class PaymentsRepository { }); } - async countLecturerPaymentRequestCount(lecturerId: number): Promise { - return await this.prismaService.payment.count({ - where: { - lecturerId, - statusId: PaymentOrderStatus.WAITING_FOR_DEPOSIT, - paymentMethodId: { - in: [PaymentMethods.현장결제, PaymentMethods.선결제], - }, - }, - }); - } - async trxDeleteUserPass( transaction: PrismaTransaction, paymentId: number, @@ -1383,4 +1300,88 @@ export class PaymentsRepository { ); } } + + // async countLecturerPaymentRequestCount(lecturerId: number): Promise { + // return await this.prismaService.payment.count({ + // where: { + // lecturerId, + // statusId: PaymentOrderStatus.WAITING_FOR_DEPOSIT, + // paymentMethodId: { + // in: [PaymentMethods.현장결제, PaymentMethods.선결제], + // }, + // }, + // }); + // } + + // async getLecturerLectureList(lecturerId: number): Promise { + // return await this.prismaService.lecture.findMany({ where: { lecturerId } }); + // } + + // async getPaymentRequestListByLecturerId(lectureId: number) { + // return await this.prismaService.payment.findMany({ + // where: { + // statusId: PaymentOrderStatus.WAITING_FOR_DEPOSIT, + // paymentMethodId: { + // in: [PaymentMethods.현장결제, PaymentMethods.선결제], + // }, + // OR: [ + // { + // reservation: { + // lectureSchedule: { lectureId }, + // }, + // }, + // { + // reservation: { + // regularLectureStatus: { lectureId }, + // }, + // }, + // ], + // }, + // include: { + // user: { include: { userProfileImage: true } }, + // paymentProductType: true, + // paymentStatus: true, + // paymentMethod: true, + // paymentCouponUsage: true, + // transferPaymentInfo: { include: { lecturerBankAccount: true } }, + // refundPaymentInfo: { + // include: { refundStatus: true, refundUserBankAccount: true }, + // }, + // reservation: { + // include: { + // lectureSchedule: true, + // regularLectureStatus: { include: { regularLectureSchedule: true } }, + // }, + // }, + // cardPaymentInfo: { include: { issuer: true, acquirer: true } }, + // virtualAccountPaymentInfo: { include: { bank: true } }, + // paymentPassUsage: { + // include: { lecturePass: true }, + // }, + // userPass: { include: { lecturePass: true } }, + // }, + // orderBy: { + // id: 'asc', + // }, + // }); + // } + + // async getPaymentRequest( + // paymentId: number, + // lecturerId: number, + // ): Promise { + // return this.prismaService.payment.findFirst({ + // where: { + // id: paymentId, + // lecturerId, + // paymentProductType: { name: PaymentProductTypes.클래스 }, + // }, + // include: { + // transferPaymentInfo: true, + // reservation: { + // include: { lectureSchedule: true, regularLectureStatus: true }, + // }, + // }, + // }); + // } } diff --git a/src/payments/services/lecturer-payments.service.ts b/src/payments/services/lecturer-payments.service.ts index 5f1b177a..3195de4e 100644 --- a/src/payments/services/lecturer-payments.service.ts +++ b/src/payments/services/lecturer-payments.service.ts @@ -17,11 +17,7 @@ import { PaymentStatusForLecturer, RefundStatuses, } from '@src/payments/constants/enum'; -import { - IPaginationParams, - PrismaTransaction, -} from '@src/common/interface/common-interface'; -import { IPayment } from '@src/payments/interface/payments.interface'; +import { IPaginationParams } from '@src/common/interface/common-interface'; import { PassSituationDto } from '@src/payments/dtos/response/pass-situation.dto'; import { GetRevenueStatisticsDto } from '../dtos/request/get-revenue-statistics.dto'; import { RevenueStatisticDto } from '../dtos/response/revenue-statistic.dto'; @@ -35,10 +31,7 @@ import { PaginatedResponse } from '@src/common/types/type'; @Injectable() export class LecturerPaymentsService { - constructor( - private readonly paymentsRepository: PaymentsRepository, - private readonly prismaService: PrismaService, - ) {} + constructor(private readonly paymentsRepository: PaymentsRepository) {} async createLecturerBankAccount( lecturerId: number, @@ -63,313 +56,6 @@ export class LecturerPaymentsService { : null; } - async getPaymentRequestList( - lecturerId: number, - ): Promise { - const lectureList: Lecture[] = - await this.paymentsRepository.getLecturerLectureList(lecturerId); - - if (!lectureList || lectureList.length === 0) { - return []; - } - - //각각의 강의에 해당하는 결제내역들을 합쳐서 반환 - const paymentList = await Promise.all( - lectureList.map(async (lecture) => { - const payments = - await this.paymentsRepository.getPaymentRequestListByLecturerId( - lecture.id, - ); - return payments.length > 0 ? { lecture, payments } : null; - }), - ); - - const finalPaymentList = paymentList.filter((item) => item !== null); - - return finalPaymentList.length > 0 - ? finalPaymentList.map((payment) => new PaymentRequestDto(payment)) - : []; - } - - async updatePaymentRequestStatus( - lecturerId: number, - dto: UpdatePaymentRequestStatusDto, - ): Promise { - const { paymentId, status, cancelAmount, refusedReason, lectureId } = dto; - - //결제 정보 확인 - const payment: IPayment = await this.checkPaymentValidity( - paymentId, - lecturerId, - status, - ); - - switch (status) { - case PaymentStatusForLecturer.DONE: - await this.processPaymentDoneStatus(payment.id); - break; - - case PaymentStatusForLecturer.REFUSED: - await this.processPaymentRefusedStatus( - payment, - cancelAmount, - refusedReason, - ); - break; - - case PaymentStatusForLecturer.WAITING_FOR_DEPOSIT: - await this.processPaymentWaitingForDePositStatus(payment, lectureId); - break; - } - } - - private async checkPaymentValidity( - paymentId: number, - lecturerId: number, - status: number, - ): Promise { - const selectedPayment: IPayment = - await this.paymentsRepository.getPaymentRequest(paymentId, lecturerId); - - if (!selectedPayment) { - throw new BadRequestException( - `잘못된 결제 정보입니다.`, - 'InvalidPayment', - ); - } - - //일반결제가 아닌 경우 - if ( - selectedPayment.paymentMethodId !== PaymentMethods.선결제 && - selectedPayment.paymentMethodId !== PaymentMethods.현장결제 - ) { - throw new BadRequestException( - `해당 결제 정보는 변경이 불가능한 결제 방식입니다.`, - 'InvalidPaymentMethod', - ); - } - - //승인일때 거절이 들어온 경우 거절일때 승인이 들어온 경우 - if ( - (selectedPayment.statusId === PaymentOrderStatus.DONE && - status === PaymentOrderStatus.REFUSED) || - (selectedPayment.statusId === PaymentOrderStatus.REFUSED && - status === PaymentOrderStatus.DONE) - ) { - throw new BadRequestException( - `해당 결제 정보는 이미 변경된 상태입니다.`, - 'PaymentStatusAlreadyUpdated', - ); - } - - if (selectedPayment.statusId === status) { - throw new BadRequestException( - `해당 결제 정보는 이미 변경된 상태입니다.`, - 'PaymentStatusAlreadyUpdated', - ); - } - - return selectedPayment; - } - private async processPaymentDoneStatus(paymentId: number): Promise { - await this.prismaService.$transaction( - async (transaction: PrismaTransaction) => { - await this.paymentsRepository.updatePaymentStatus( - paymentId, - PaymentStatusForLecturer.DONE, - transaction, - ); - - await this.paymentsRepository.trxUpdateReservationEnabled( - transaction, - paymentId, - true, - ); - }, - ); - } - - private async processPaymentRefusedStatus( - payment: IPayment, - cancelAmount: number, - refusedReason: string, - ): Promise { - await this.compareCancelAmount(payment, cancelAmount); - - await this.prismaService.$transaction( - async (transaction: PrismaTransaction) => { - await this.paymentsRepository.updatePaymentStatus( - payment.id, - PaymentStatusForLecturer.REFUSED, - transaction, - ); - - await this.paymentsRepository.trxUpdateTransferPayment( - transaction, - payment.id, - { - cancelAmount, - refusedReason, - refundStatusId: RefundStatuses.PENDING, - }, - ); - - await this.trxRollbackReservationRelatedData( - transaction, - payment, - false, - ); - }, - ); - } - - private async compareCancelAmount( - payment: IPayment, - clientCancelAmount, - ): Promise { - let targetAmount; - - //현장 결제일때는 보증금, 현장일때는 최종 결제 금액 비교 - switch (payment.paymentMethodId) { - case PaymentMethods.선결제: - targetAmount = payment.finalPrice; - break; - case PaymentMethods.현장결제: - targetAmount = payment.transferPaymentInfo.noShowDeposit; - break; - } - - if (targetAmount !== clientCancelAmount) { - throw new BadRequestException( - `환불금액이 올바르지 않습니다.`, - 'InvalidRefundAmount', - ); - } - } - - private async trxRollbackReservationRelatedData( - transaction: PrismaTransaction, - payment: IPayment, - isIncrement: boolean, - lectureMaxCapacity?: number, - ): Promise { - const { reservation } = payment; - let lectureMethod; - let numberOfParticipants; - if (reservation.lectureScheduleId) { - lectureMethod = LectureMethod.원데이; - numberOfParticipants = reservation.lectureSchedule.numberOfParticipants; - } else { - lectureMethod = LectureMethod.정기; - numberOfParticipants = - reservation.regularLectureStatus.numberOfParticipants; - } - - const trxUpdateParticipantsMethod = isIncrement - ? this.paymentsRepository.trxIncrementLectureScheduleParticipants - : this.paymentsRepository.trxDecrementLectureScheduleParticipants; - - const trxUpdateLearnerCountMethod = isIncrement - ? this.paymentsRepository.trxIncrementLectureLearner - : this.paymentsRepository.trxDecrementLectureLearnerEnrollmentCount; - - //각 스케쥴의 현재 인원 수정 - //되돌릴 때 신청한 인원이 초과되면 에러 반환 및 롤백 취소 - if (isIncrement && lectureMaxCapacity) { - const remainingCapacity = lectureMaxCapacity - numberOfParticipants; - - if (remainingCapacity < reservation.participants) { - throw new BadRequestException( - `최대 인원 초과로 인해 취소할 수 없습니다.`, - 'ExceededMaxParticipants', - ); - } - } - - await trxUpdateParticipantsMethod(transaction, lectureMethod, reservation); - - //수강생의 신청 횟수 수정 - await trxUpdateLearnerCountMethod( - transaction, - payment.userId, - payment.lecturerId, - ); - } - - private async processPaymentWaitingForDePositStatus( - payment: IPayment, - lectureId: number, - ): Promise { - switch (payment.statusId) { - case PaymentOrderStatus.DONE: - await this.rollbackPaymentDoneStatus(payment.id); - break; - case PaymentOrderStatus.REFUSED: - await this.rollbackPaymentRefusedStatus(payment, lectureId); - break; - } - } - - private async rollbackPaymentDoneStatus(paymentId: number) { - await this.prismaService.$transaction( - async (transaction: PrismaTransaction) => { - await this.paymentsRepository.updatePaymentStatus( - paymentId, - PaymentStatusForLecturer.WAITING_FOR_DEPOSIT, - transaction, - ); - - await this.paymentsRepository.trxUpdateReservationEnabled( - transaction, - paymentId, - false, - ); - }, - ); - } - - private async rollbackPaymentRefusedStatus( - payment: IPayment, - lectureId: number, - ): Promise { - const lecture: Lecture = await this.paymentsRepository.getLecture( - lectureId, - ); - - await this.prismaService.$transaction( - async (transaction: PrismaTransaction) => { - await this.paymentsRepository.updatePaymentStatus( - payment.id, - PaymentStatusForLecturer.WAITING_FOR_DEPOSIT, - transaction, - ); - - await this.paymentsRepository.trxUpdateTransferPayment( - transaction, - payment.id, - { - refundStatusId: RefundStatuses.NONE, - cancelAmount: null, - refusedReason: null, - }, - ); - - await this.trxRollbackReservationRelatedData( - transaction, - payment, - true, - lecture.maxCapacity, - ); - }, - ); - } - - async getPaymentRequestCount(lecturerId: number): Promise { - return await this.paymentsRepository.countLecturerPaymentRequestCount( - lecturerId, - ); - } - async getPassSituation( lecturerId: number, passId: number, @@ -438,10 +124,12 @@ export class LecturerPaymentsService { if (statisticsType === 'MONTHLY') { return await this.getMonthlyRevenue(lecturerId); - } else if (statisticsType === 'DAILY') { + } + + if (statisticsType === 'DAILY') { const endDate = date ? new Date(date) : new Date(); const startDate = date ? new Date(date) : new Date(); - startDate.setDate(endDate.getDate() - 30); + startDate.setDate(startDate.getDate() - 30); return await this.getDailyRevenue(lecturerId, startDate, endDate); } @@ -451,22 +139,20 @@ export class LecturerPaymentsService { lecturerId: number, startDate: Date, endDate: Date, - ) { + ): Promise { const dailyRevenue = []; while (startDate <= endDate) { - startDate.setHours(9, 0, 0); // startDate를 00시 00분 00초로 설정 - - const nextDate = new Date(startDate); - nextDate.setHours(32, 59, 59); // nextDate를 23시 59분 59초로 설정 + const { convertedStartDate, convertedEndDate } = + DateUtils.getUTCStartAndEndOfRange(startDate); const { totalSales, totalPrice } = await this.getRevenueForDate( lecturerId, - startDate, - nextDate, + convertedStartDate, + convertedEndDate, ); - const formattedDate = startDate.toISOString().slice(0, 10); // "yyyy-mm-dd" 형식으로 변환 + const formattedDate = convertedStartDate.toISOString().slice(0, 10); // "yyyy-mm-dd" 형식으로 변환 dailyRevenue.push({ date: formattedDate, totalSales, totalPrice }); startDate.setDate(startDate.getDate() + 1); @@ -480,20 +166,21 @@ export class LecturerPaymentsService { const monthlyRevenues = []; const currentYear = currentDate.getFullYear(); - const currentMonth = currentDate.getMonth() + 1; + const currentMonth = currentDate.getMonth(); for (let i = 0; i < 12; i++) { const year = currentMonth - i >= 0 ? currentYear : currentYear - 1; const month = (currentMonth - i + 12) % 12; - const startDate = new Date(year, month - 1, 1, 9); // 각 월의 시작일 - const nextDate = new Date(year, month, 0, 9); // 각 월의 마지막일 + const { convertedStartDate, convertedEndDate } = + DateUtils.getUTCStartAndEndOfMonth(year, month); + const { totalSales, totalPrice } = await this.getRevenueForDate( lecturerId, - startDate, - nextDate, + convertedStartDate, + convertedEndDate, ); - const formattedDate = startDate.toISOString().slice(0, 7); // "yyyy-mm" 형식으로 변환 + const formattedDate = convertedStartDate.toISOString().slice(0, 7); // "yyyy-mm" 형식으로 변환 monthlyRevenues.push({ date: formattedDate, totalSales, @@ -520,9 +207,7 @@ export class LecturerPaymentsService { 0, ); - const totalSales = revenue.length; - - return { totalSales, totalPrice }; + return { totalSales: revenue.length, totalPrice }; } async getLecturerPaymentList( @@ -534,10 +219,11 @@ export class LecturerPaymentsService { const paymentProductTypeId = this.getPaymentTypeId(productType); - const { startOfDay, endOfDay } = DateUtils.getUTCStartAndEndOfRange( - new Date(startDate), - new Date(endDate), - ); + const { convertedStartDate, convertedEndDate } = + DateUtils.getUTCStartAndEndOfRange( + new Date(startDate), + new Date(endDate), + ); const paginationParams: IPaginationParams = generatePaginationParams(paginationOptions); @@ -546,8 +232,8 @@ export class LecturerPaymentsService { await this.paymentsRepository.getLecturerPaymentCount( lecturerId, paymentProductTypeId, - startOfDay, - endOfDay, + convertedStartDate, + convertedEndDate, lectureId, ); @@ -559,8 +245,8 @@ export class LecturerPaymentsService { await this.paymentsRepository.getLecturerPaymentList( lecturerId, paymentProductTypeId, - startOfDay, - endOfDay, + convertedStartDate, + convertedEndDate, paginationParams, lectureId, ); @@ -576,16 +262,17 @@ export class LecturerPaymentsService { const paymentProductTypeId = this.getPaymentTypeId(productType); - const { startOfDay, endOfDay } = DateUtils.getUTCStartAndEndOfRange( - new Date(startDate), - new Date(endDate), - ); + const { convertedStartDate, convertedEndDate } = + DateUtils.getUTCStartAndEndOfRange( + new Date(startDate), + new Date(endDate), + ); return await this.paymentsRepository.getLecturerPaymentTotalRevenue( lecturerId, paymentProductTypeId, - startOfDay, - endOfDay, + convertedStartDate, + convertedEndDate, lectureId, ); } @@ -597,4 +284,313 @@ export class LecturerPaymentsService { ? undefined : paymentHistoryType; } + + // async getPaymentRequestList( + // lecturerId: number, + // ): Promise { + // const lectureList: Lecture[] = + // await this.paymentsRepository.getLecturerLectureList(lecturerId); + + // if (!lectureList || lectureList.length === 0) { + // return []; + // } + + // //각각의 강의에 해당하는 결제내역들을 합쳐서 반환 + // const paymentList = await Promise.all( + // lectureList.map(async (lecture) => { + // const payments = + // await this.paymentsRepository.getPaymentRequestListByLecturerId( + // lecture.id, + // ); + // return payments.length > 0 ? { lecture, payments } : null; + // }), + // ); + + // const finalPaymentList = paymentList.filter((item) => item !== null); + + // return finalPaymentList.length > 0 + // ? finalPaymentList.map((payment) => new PaymentRequestDto(payment)) + // : []; + // } + + // async updatePaymentRequestStatus( + // lecturerId: number, + // dto: UpdatePaymentRequestStatusDto, + // ): Promise { + // const { paymentId, status, cancelAmount, refusedReason, lectureId } = dto; + + // //결제 정보 확인 + // const payment: IPayment = await this.checkPaymentValidity( + // paymentId, + // lecturerId, + // status, + // ); + + // switch (status) { + // case PaymentStatusForLecturer.DONE: + // await this.processPaymentDoneStatus(payment.id); + // break; + + // case PaymentStatusForLecturer.REFUSED: + // await this.processPaymentRefusedStatus( + // payment, + // cancelAmount, + // refusedReason, + // ); + // break; + + // case PaymentStatusForLecturer.WAITING_FOR_DEPOSIT: + // await this.processPaymentWaitingForDePositStatus(payment, lectureId); + // break; + // } + // } + + // private async checkPaymentValidity( + // paymentId: number, + // lecturerId: number, + // status: number, + // ): Promise { + // const selectedPayment: IPayment = + // await this.paymentsRepository.getPaymentRequest(paymentId, lecturerId); + + // if (!selectedPayment) { + // throw new BadRequestException( + // `잘못된 결제 정보입니다.`, + // 'InvalidPayment', + // ); + // } + + // //일반결제가 아닌 경우 + // if ( + // selectedPayment.paymentMethodId !== PaymentMethods.선결제 && + // selectedPayment.paymentMethodId !== PaymentMethods.현장결제 + // ) { + // throw new BadRequestException( + // `해당 결제 정보는 변경이 불가능한 결제 방식입니다.`, + // 'InvalidPaymentMethod', + // ); + // } + + // //승인일때 거절이 들어온 경우 거절일때 승인이 들어온 경우 + // if ( + // (selectedPayment.statusId === PaymentOrderStatus.DONE && + // status === PaymentOrderStatus.REFUSED) || + // (selectedPayment.statusId === PaymentOrderStatus.REFUSED && + // status === PaymentOrderStatus.DONE) + // ) { + // throw new BadRequestException( + // `해당 결제 정보는 이미 변경된 상태입니다.`, + // 'PaymentStatusAlreadyUpdated', + // ); + // } + + // if (selectedPayment.statusId === status) { + // throw new BadRequestException( + // `해당 결제 정보는 이미 변경된 상태입니다.`, + // 'PaymentStatusAlreadyUpdated', + // ); + // } + + // return selectedPayment; + // } + + // private async processPaymentDoneStatus(paymentId: number): Promise { + // await this.prismaService.$transaction( + // async (transaction: PrismaTransaction) => { + // await this.paymentsRepository.updatePaymentStatus( + // paymentId, + // PaymentStatusForLecturer.DONE, + // transaction, + // ); + + // await this.paymentsRepository.trxUpdateReservationEnabled( + // transaction, + // paymentId, + // true, + // ); + // }, + // ); + // } + + // private async processPaymentRefusedStatus( + // payment: IPayment, + // cancelAmount: number, + // refusedReason: string, + // ): Promise { + // await this.compareCancelAmount(payment, cancelAmount); + + // await this.prismaService.$transaction( + // async (transaction: PrismaTransaction) => { + // await this.paymentsRepository.updatePaymentStatus( + // payment.id, + // PaymentStatusForLecturer.REFUSED, + // transaction, + // ); + + // await this.paymentsRepository.trxUpdateTransferPayment( + // transaction, + // payment.id, + // { + // cancelAmount, + // refusedReason, + // refundStatusId: RefundStatuses.PENDING, + // }, + // ); + + // await this.trxRollbackReservationRelatedData( + // transaction, + // payment, + // false, + // ); + // }, + // ); + // } + + // private async compareCancelAmount( + // payment: IPayment, + // clientCancelAmount, + // ): Promise { + // let targetAmount; + + // //현장 결제일때는 보증금, 현장일때는 최종 결제 금액 비교 + // switch (payment.paymentMethodId) { + // case PaymentMethods.선결제: + // targetAmount = payment.finalPrice; + // break; + // case PaymentMethods.현장결제: + // targetAmount = payment.transferPaymentInfo.noShowDeposit; + // break; + // } + + // if (targetAmount !== clientCancelAmount) { + // throw new BadRequestException( + // `환불금액이 올바르지 않습니다.`, + // 'InvalidRefundAmount', + // ); + // } + // } + + // private async trxRollbackReservationRelatedData( + // transaction: PrismaTransaction, + // payment: IPayment, + // isIncrement: boolean, + // lectureMaxCapacity?: number, + // ): Promise { + // const { reservation } = payment; + // let lectureMethod; + // let numberOfParticipants; + + // if (reservation.lectureScheduleId) { + // lectureMethod = LectureMethod.원데이; + // numberOfParticipants = reservation.lectureSchedule.numberOfParticipants; + // } else { + // lectureMethod = LectureMethod.정기; + // numberOfParticipants = + // reservation.regularLectureStatus.numberOfParticipants; + // } + + // const trxUpdateParticipantsMethod = isIncrement + // ? this.paymentsRepository.trxIncrementLectureScheduleParticipants + // : this.paymentsRepository.trxDecrementLectureScheduleParticipants; + + // const trxUpdateLearnerCountMethod = isIncrement + // ? this.paymentsRepository.trxIncrementLectureLearner + // : this.paymentsRepository.trxDecrementLectureLearnerEnrollmentCount; + + // //각 스케쥴의 현재 인원 수정 + // //되돌릴 때 신청한 인원이 초과되면 에러 반환 및 롤백 취소 + // if (isIncrement && lectureMaxCapacity) { + // const remainingCapacity = lectureMaxCapacity - numberOfParticipants; + + // if (remainingCapacity < reservation.participants) { + // throw new BadRequestException( + // `최대 인원 초과로 인해 취소할 수 없습니다.`, + // 'ExceededMaxParticipants', + // ); + // } + // } + + // await trxUpdateParticipantsMethod(transaction, lectureMethod, reservation); + + // //수강생의 신청 횟수 수정 + // await trxUpdateLearnerCountMethod( + // transaction, + // payment.userId, + // payment.lecturerId, + // ); + // } + + // private async processPaymentWaitingForDePositStatus( + // payment: IPayment, + // lectureId: number, + // ): Promise { + // switch (payment.statusId) { + // case PaymentOrderStatus.DONE: + // await this.rollbackPaymentDoneStatus(payment.id); + // break; + // case PaymentOrderStatus.REFUSED: + // await this.rollbackPaymentRefusedStatus(payment, lectureId); + // break; + // } + // } + + // private async rollbackPaymentDoneStatus(paymentId: number) { + // await this.prismaService.$transaction( + // async (transaction: PrismaTransaction) => { + // await this.paymentsRepository.updatePaymentStatus( + // paymentId, + // PaymentStatusForLecturer.WAITING_FOR_DEPOSIT, + // transaction, + // ); + + // await this.paymentsRepository.trxUpdateReservationEnabled( + // transaction, + // paymentId, + // false, + // ); + // }, + // ); + // } + + // private async rollbackPaymentRefusedStatus( + // payment: IPayment, + // lectureId: number, + // ): Promise { + // const lecture: Lecture = await this.paymentsRepository.getLecture( + // lectureId, + // ); + + // await this.prismaService.$transaction( + // async (transaction: PrismaTransaction) => { + // await this.paymentsRepository.updatePaymentStatus( + // payment.id, + // PaymentStatusForLecturer.WAITING_FOR_DEPOSIT, + // transaction, + // ); + + // await this.paymentsRepository.trxUpdateTransferPayment( + // transaction, + // payment.id, + // { + // refundStatusId: RefundStatuses.NONE, + // cancelAmount: null, + // refusedReason: null, + // }, + // ); + + // await this.trxRollbackReservationRelatedData( + // transaction, + // payment, + // true, + // lecture.maxCapacity, + // ); + // }, + // ); + // } + + // async getPaymentRequestCount(lecturerId: number): Promise { + // return await this.paymentsRepository.countLecturerPaymentRequestCount( + // lecturerId, + // ); + // } } diff --git a/test/unit/utils/dateUtils.spec.ts b/test/unit/utils/dateUtils.spec.ts index 44cde38c..bb168c73 100644 --- a/test/unit/utils/dateUtils.spec.ts +++ b/test/unit/utils/dateUtils.spec.ts @@ -1,63 +1,50 @@ import { DateUtils } from '@src/common/utils/date.utils'; describe('DateUtils', () => { + const year = 2023; + const month = 0; // 달은 -1 해서 사용해야 함 5월이면 4로 + const startDay = 1; + const endDay = 25; + describe('getUTCStartAndEndOfRange', () => { - it('날짜가 제공되지 않으면 현재 날짜의 시작과 끝을 반환해야 한다', () => { - const { startOfDay, endOfDay } = DateUtils.getUTCStartAndEndOfRange(); + it('날짜가 제공되지 않으면 현재 날짜의 00시와 23시59분 반환', () => { + const { convertedStartDate, convertedEndDate } = + DateUtils.getUTCStartAndEndOfRange(); const now = new Date(); - expect(startOfDay.getUTCFullYear()).toBe(now.getUTCFullYear()); - expect(startOfDay.getUTCMonth()).toBe(now.getUTCMonth()); - expect(startOfDay.getUTCDate()).toBe(now.getUTCDate()); - expect(startOfDay.getUTCHours()).toBe(0); - expect(startOfDay.getUTCMinutes()).toBe(0); - expect(startOfDay.getUTCSeconds()).toBe(0); - expect(startOfDay.getUTCMilliseconds()).toBe(0); - - expect(endOfDay.getUTCFullYear()).toBe(now.getUTCFullYear()); - expect(endOfDay.getUTCMonth()).toBe(now.getUTCMonth()); - expect(endOfDay.getUTCDate()).toBe(now.getUTCDate()); - expect(endOfDay.getUTCHours()).toBe(23); - expect(endOfDay.getUTCMinutes()).toBe(59); - expect(endOfDay.getUTCSeconds()).toBe(59); - expect(endOfDay.getUTCMilliseconds()).toBe(999); + expect(convertedStartDate.getTime()).toBe(now.setUTCHours(0, 0, 0, 0)); + expect(convertedEndDate.getTime()).toBe(now.setUTCHours(23, 59, 59, 999)); }); - it('제공된 날짜의 시작과 끝을 반환해야 한다', () => { - const startDate = new Date(Date.UTC(2023, 0, 1)); - const endDate = new Date(Date.UTC(2023, 0, 1)); - const { startOfDay, endOfDay } = DateUtils.getUTCStartAndEndOfRange( - startDate, - endDate, - ); + it('제공된 날짜의 00시와 23시59분을 반환', () => { + const startDate = new Date(Date.UTC(year, month, startDay)); + const endDate = new Date(Date.UTC(year, month, endDay)); - expect(startOfDay.getTime()).toBe(startDate.setUTCHours(0, 0, 0, 0)); - expect(endOfDay.getTime()).toBe(endDate.setUTCHours(23, 59, 59, 999)); + const { convertedStartDate, convertedEndDate } = + DateUtils.getUTCStartAndEndOfRange(startDate, endDate); + + expect(convertedStartDate.getTime()).toBe( + startDate.setUTCHours(0, 0, 0, 0), + ); + expect(convertedEndDate.getTime()).toBe( + endDate.setUTCHours(23, 59, 59, 999), + ); }); }); describe('getUTCStartAndEndOfMonth', () => { - it('주어진 달의 시작과 끝을 반환해야 한다', () => { - const { startOfMonth, endOfMonth } = DateUtils.getUTCStartAndEndOfMonth( - 2023, - 0, - ); + it('주어진 달의 1일과 마지막 일을 반환', () => { + const { convertedStartDate, convertedEndDate } = + DateUtils.getUTCStartAndEndOfMonth(year, month); + + const startOfMonthExpected = new Date(Date.UTC(year, month, 1)); + startOfMonthExpected.setUTCHours(0, 0, 0, 0); + + const endOfMonthExpected = new Date(Date.UTC(year, month + 1, 0)); + endOfMonthExpected.setUTCHours(23, 59, 59, 999); - expect(startOfMonth.getUTCFullYear()).toBe(2023); - expect(startOfMonth.getUTCMonth()).toBe(0); - expect(startOfMonth.getUTCDate()).toBe(1); - expect(startOfMonth.getUTCHours()).toBe(0); - expect(startOfMonth.getUTCMinutes()).toBe(0); - expect(startOfMonth.getUTCSeconds()).toBe(0); - expect(startOfMonth.getUTCMilliseconds()).toBe(0); - - expect(endOfMonth.getUTCFullYear()).toBe(2023); - expect(endOfMonth.getUTCMonth()).toBe(0); - expect(endOfMonth.getUTCDate()).toBe(31); - expect(endOfMonth.getUTCHours()).toBe(23); - expect(endOfMonth.getUTCMinutes()).toBe(59); - expect(endOfMonth.getUTCSeconds()).toBe(59); - expect(endOfMonth.getUTCMilliseconds()).toBe(999); + expect(convertedStartDate.getTime()).toBe(startOfMonthExpected.getTime()); + expect(convertedEndDate.getTime()).toBe(endOfMonthExpected.getTime()); }); }); });