diff --git a/src/main/kotlin/roomescape/reservation/business/MyReservationFindService.kt b/src/main/kotlin/roomescape/reservation/business/MyReservationFindService.kt new file mode 100644 index 00000000..7f0c9e24 --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/business/MyReservationFindService.kt @@ -0,0 +1,54 @@ +package roomescape.reservation.business + +import io.github.oshai.kotlinlogging.KLogger +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import roomescape.payment.implement.PaymentFinderV2 +import roomescape.payment.infrastructure.persistence.v2.CanceledPaymentEntityV2 +import roomescape.payment.infrastructure.persistence.v2.PaymentDetailEntity +import roomescape.payment.infrastructure.persistence.v2.PaymentEntityV2 +import roomescape.reservation.implement.ReservationFinder +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.reservation.web.ReservationDetailRetrieveResponse +import roomescape.reservation.web.ReservationSummaryRetrieveListResponse +import roomescape.reservation.web.toCancelDetailResponse +import roomescape.reservation.web.toPaymentDetailResponse +import roomescape.reservation.web.toReservationDetailRetrieveResponse +import roomescape.reservation.web.toRetrieveResponse +import roomescape.reservation.web.toSummaryListResponse + +private val log: KLogger = KotlinLogging.logger {} + +@Service +class MyReservationFindService( + private val reservationFinder: ReservationFinder, + private val paymentFinder: PaymentFinderV2 +) { + + @Transactional(readOnly = true) + fun findReservationsByMemberId(memberId: Long): ReservationSummaryRetrieveListResponse { + log.debug { "[ReservationFindServiceV2.findReservationsByMemberId] 시작: memberId=$memberId" } + + return reservationFinder.findAllByMemberIdV2(memberId) + .toSummaryListResponse() + .also { log.info { "[ReservationFindServiceV2.findReservationsByMemberId] ${it.reservations.size}개의 예약 조회 완료: memberId=$memberId" } } + } + + @Transactional(readOnly = true) + fun showReservationDetails(reservationId: Long): ReservationDetailRetrieveResponse { + log.debug { "[ReservationFindServiceV2.showReservationDetails] 시작: reservationId=$reservationId" } + + val reservation: ReservationEntity = reservationFinder.findById(reservationId) + val payment: PaymentEntityV2 = paymentFinder.findPaymentByReservationId(reservationId) + val paymentDetail: PaymentDetailEntity = paymentFinder.findPaymentDetailByPaymentId(payment.id) + val canceledPayment: CanceledPaymentEntityV2? = paymentFinder.findCanceledPaymentByPaymentIdOrNull(payment.id) + + return reservation.toReservationDetailRetrieveResponse( + payment = payment.toRetrieveResponse(detail = paymentDetail.toPaymentDetailResponse()), + cancellation = canceledPayment?.toCancelDetailResponse() + ).also { + log.info { "[ReservationFindServiceV2.showReservationDetails] 예약 상세 조회 완료: reservationId=$reservationId" } + } + } +} diff --git a/src/main/kotlin/roomescape/reservation/docs/MyReservationAPI.kt b/src/main/kotlin/roomescape/reservation/docs/MyReservationAPI.kt new file mode 100644 index 00000000..62799114 --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/docs/MyReservationAPI.kt @@ -0,0 +1,33 @@ +package roomescape.reservation.docs + +import io.swagger.v3.oas.annotations.Operation +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.responses.ApiResponse +import io.swagger.v3.oas.annotations.responses.ApiResponses +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PathVariable +import roomescape.auth.web.support.LoginRequired +import roomescape.auth.web.support.MemberId +import roomescape.common.dto.response.CommonApiResponse +import roomescape.reservation.web.ReservationDetailRetrieveResponse +import roomescape.reservation.web.ReservationSummaryRetrieveListResponse + +interface MyReservationAPI { + @LoginRequired + @Operation(summary = "내 예약 개요 조회", tags = ["로그인이 필요한 API"]) + @ApiResponses( + ApiResponse(responseCode = "200", description = "성공"), + ) + fun findAllMyReservations( + @MemberId @Parameter(hidden = true) memberId: Long + ): ResponseEntity> + + @LoginRequired + @Operation(summary = "예약 상세 조회", tags = ["로그인이 필요한 API"]) + @ApiResponses( + ApiResponse(responseCode = "200", description = "성공"), + ) + fun showReservationDetails( + @PathVariable("id") reservationId: Long + ): ResponseEntity> +} diff --git a/src/main/kotlin/roomescape/reservation/web/MyReservationController.kt b/src/main/kotlin/roomescape/reservation/web/MyReservationController.kt new file mode 100644 index 00000000..b0feffd7 --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/web/MyReservationController.kt @@ -0,0 +1,35 @@ +package roomescape.reservation.web + +import io.swagger.v3.oas.annotations.Parameter +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RestController +import roomescape.auth.web.support.MemberId +import roomescape.common.dto.response.CommonApiResponse +import roomescape.reservation.business.MyReservationFindService +import roomescape.reservation.docs.MyReservationAPI + +@RestController +class MyReservationController( + private val reservationFindService: MyReservationFindService +) : MyReservationAPI { + + @GetMapping("/v2/reservations") + override fun findAllMyReservations( + @MemberId @Parameter(hidden=true) memberId: Long + ): ResponseEntity> { + val response = reservationFindService.findReservationsByMemberId(memberId) + + return ResponseEntity.ok(CommonApiResponse(response)) + } + + @GetMapping("/v2/reservations/{id}/details") + override fun showReservationDetails( + @PathVariable("id") reservationId: Long + ): ResponseEntity> { + val response = reservationFindService.showReservationDetails(reservationId) + + return ResponseEntity.ok(CommonApiResponse(response)) + } +} diff --git a/src/main/kotlin/roomescape/reservation/web/MyReservationResponse.kt b/src/main/kotlin/roomescape/reservation/web/MyReservationResponse.kt new file mode 100644 index 00000000..27668c15 --- /dev/null +++ b/src/main/kotlin/roomescape/reservation/web/MyReservationResponse.kt @@ -0,0 +1,196 @@ +package roomescape.reservation.web + +import roomescape.member.infrastructure.persistence.MemberEntity +import roomescape.payment.exception.PaymentErrorCode +import roomescape.payment.exception.PaymentException +import roomescape.payment.infrastructure.common.PaymentStatus +import roomescape.payment.infrastructure.persistence.v2.CanceledPaymentEntityV2 +import roomescape.payment.infrastructure.persistence.v2.PaymentBankTransferDetailEntity +import roomescape.payment.infrastructure.persistence.v2.PaymentCardDetailEntity +import roomescape.payment.infrastructure.persistence.v2.PaymentDetailEntity +import roomescape.payment.infrastructure.persistence.v2.PaymentEasypayPrepaidDetailEntity +import roomescape.payment.infrastructure.persistence.v2.PaymentEntityV2 +import roomescape.reservation.infrastructure.persistence.ReservationEntity +import roomescape.reservation.infrastructure.persistence.ReservationStatus +import roomescape.reservation.web.PaymentDetailResponse.BankTransferDetailResponse +import roomescape.reservation.web.PaymentDetailResponse.CardDetailResponse +import roomescape.reservation.web.PaymentDetailResponse.EasyPayPrepaidDetailResponse +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.OffsetDateTime +import kotlin.Int + +data class ReservationSummaryRetrieveResponse( + val id: Long, + val themeName: String, + val date: LocalDate, + val startAt: LocalTime, + val status: ReservationStatus +) + +fun ReservationEntity.toReservationSummaryRetrieveResponse(): ReservationSummaryRetrieveResponse { + return ReservationSummaryRetrieveResponse( + id = this.id!!, + themeName = this.theme.name, + date = this.date, + startAt = this.time.startAt, + status = this.status + ) +} + +data class ReservationSummaryRetrieveListResponse( + val reservations: List +) + +fun List.toSummaryListResponse(): ReservationSummaryRetrieveListResponse { + return ReservationSummaryRetrieveListResponse( + reservations = this.map { it.toReservationSummaryRetrieveResponse() } + ) +} + +data class ReservationDetailRetrieveResponse( + val id: Long, + val user: UserDetailRetrieveResponse, + val themeName: String, + val date: LocalDate, + val startAt: LocalTime, + val applicationDateTime: LocalDateTime, + val payment: PaymentRetrieveResponse, + val cancellation: PaymentCancelDetailResponse? = null +) + +data class UserDetailRetrieveResponse( + val id: Long, + val name: String, + val email: String +) + +fun MemberEntity.toUserDetailRetrieveResponse(): UserDetailRetrieveResponse { + return UserDetailRetrieveResponse( + id = this.id!!, + name = this.name, + email = this.email + ) +} + +fun ReservationEntity.toReservationDetailRetrieveResponse( + payment: PaymentRetrieveResponse, + cancellation: PaymentCancelDetailResponse? = null +): ReservationDetailRetrieveResponse { + return ReservationDetailRetrieveResponse( + id = this.id!!, + user = this.member.toUserDetailRetrieveResponse(), + themeName = this.theme.name, + date = this.date, + startAt = this.time.startAt, + applicationDateTime = this.createdAt!!, + payment = payment, + cancellation = cancellation, + ) +} + +data class PaymentRetrieveResponse( + val orderId: String, + val totalAmount: Int, + val method: String, + val status: PaymentStatus, + val requestedAt: OffsetDateTime, + val approvedAt: OffsetDateTime, + val detail: PaymentDetailResponse, +) + +fun PaymentEntityV2.toRetrieveResponse(detail: PaymentDetailResponse): PaymentRetrieveResponse { + return PaymentRetrieveResponse( + orderId = this.orderId, + totalAmount = this.totalAmount, + method = this.method.koreanName, + status = this.status, + requestedAt = this.requestedAt, + approvedAt = this.approvedAt, + detail = detail + ) +} + +sealed class PaymentDetailResponse { + + data class CardDetailResponse( + val type: String = "CARD", + val issuerCode: String, + val cardType: String, + val ownerType: String, + val cardNumber: String, + val amount: Int, + val approvalNumber: String, + val installmentPlanMonths: Int, + val easypayProviderName: String?, + val easypayDiscountAmount: Int?, + ) : PaymentDetailResponse() + + + data class BankTransferDetailResponse( + val type: String = "BANK_TRANSFER", + val bankName: String, + ) : PaymentDetailResponse() + + + data class EasyPayPrepaidDetailResponse( + val type: String = "EASYPAY_PREPAID", + val providerName: String, + val amount: Int, + val discountAmount: Int, + ) : PaymentDetailResponse() +} + +fun PaymentDetailEntity.toPaymentDetailResponse(): PaymentDetailResponse { + return when (this) { + is PaymentCardDetailEntity -> this.toCardDetailResponse() + is PaymentBankTransferDetailEntity -> this.toBankTransferDetailResponse() + is PaymentEasypayPrepaidDetailEntity -> this.toEasyPayPrepaidDetailResponse() + else -> throw PaymentException(PaymentErrorCode.NOT_SUPPORTED_PAYMENT_TYPE) + } +} + +fun PaymentCardDetailEntity.toCardDetailResponse(): CardDetailResponse { + return CardDetailResponse( + issuerCode = this.issuerCode.koreanName, + cardType = this.cardType.koreanName, + ownerType = this.ownerType.koreanName, + cardNumber = this.cardNumber, + amount = this.amount, + approvalNumber = this.approvalNumber, + installmentPlanMonths = this.installmentPlanMonths, + easypayProviderName = this.easypayProviderCode?.koreanName, + easypayDiscountAmount = this.easypayDiscountAmount + ) +} + +fun PaymentBankTransferDetailEntity.toBankTransferDetailResponse(): BankTransferDetailResponse { + return BankTransferDetailResponse( + bankName = this.bankCode.koreanName + ) +} + +fun PaymentEasypayPrepaidDetailEntity.toEasyPayPrepaidDetailResponse(): EasyPayPrepaidDetailResponse { + return EasyPayPrepaidDetailResponse( + providerName = this.easypayProviderCode.koreanName, + amount = this.amount, + discountAmount = this.discountAmount + ) +} + +data class PaymentCancelDetailResponse( + val cancellationRequestedAt: LocalDateTime, + val cancellationApprovedAt: OffsetDateTime?, + val cancelReason: String, + val canceledBy: Long, +) + +fun CanceledPaymentEntityV2.toCancelDetailResponse(): PaymentCancelDetailResponse { + return PaymentCancelDetailResponse( + cancellationRequestedAt = this.requestedAt, + cancellationApprovedAt = this.canceledAt, + cancelReason = this.cancelReason, + canceledBy = this.canceledBy + ) +}