diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt index 434ced3..82885d9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/CanPaymentService.kt @@ -1,6 +1,9 @@ package kr.co.vividnext.sodalive.can.payment +import kr.co.vividnext.sodalive.can.CanRepository +import kr.co.vividnext.sodalive.can.charge.Charge import kr.co.vividnext.sodalive.can.charge.ChargeRepository +import kr.co.vividnext.sodalive.can.charge.ChargeStatus import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.SpentCan import kr.co.vividnext.sodalive.can.use.TotalSpentCan @@ -18,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional @Service class CanPaymentService( + private val repository: CanRepository, private val memberRepository: MemberRepository, private val chargeRepository: ChargeRepository, private val useCanRepository: UseCanRepository, @@ -242,4 +246,41 @@ class CanPaymentService( TotalSpentCan(total = 0) } } + + @Transactional + fun refund(memberId: Long, roomId: Long) { + val member = memberRepository.findByIdOrNull(memberId) + ?: throw SodaException("잘못된 예약정보 입니다.") + + val useCan = repository.getCanUsedForLiveRoomNotRefund( + memberId = memberId, + roomId = roomId, + canUsage = CanUsage.LIVE + ) ?: throw SodaException("잘못된 예약정보 입니다.") + useCan.isRefund = true + + val useCoinCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!) + useCoinCalculates.forEach { + it.status = UseCanCalculateStatus.REFUND + val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) + charge.title = "${it.can} 캔" + charge.useCan = useCan + + when (it.paymentGateway) { + PaymentGateway.PG -> member.pgRewardCan += charge.rewardCan + PaymentGateway.GOOGLE_IAP -> member.googleRewardCan += charge.rewardCan + PaymentGateway.APPLE_IAP -> member.appleRewardCan += charge.rewardCan + } + charge.member = member + + val payment = Payment( + status = PaymentStatus.COMPLETE, + paymentGateway = it.paymentGateway + ) + payment.method = "환불" + charge.payment = payment + + chargeRepository.save(charge) + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/CancelLiveReservationRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/CancelLiveReservationRequest.kt new file mode 100644 index 0000000..edcf504 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/CancelLiveReservationRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.live.reservation + +data class CancelLiveReservationRequest(val reservationId: Long, val reason: String) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/GetLiveReservationResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/GetLiveReservationResponse.kt new file mode 100644 index 0000000..65627d1 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/GetLiveReservationResponse.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.live.reservation + +data class GetLiveReservationResponse( + val reservationId: Long, + val roomId: Long, + val title: String, + val coverImageUrl: String, + val price: Int, + val masterNickname: String, + val beginDateTime: String, + val cancelable: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancel.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancel.kt new file mode 100644 index 0000000..1154988 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancel.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.live.reservation + +import kr.co.vividnext.sodalive.common.BaseEntity +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.OneToOne + +@Entity +data class LiveReservationCancel( + val reason: String +) : BaseEntity() { + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "reservation_id", nullable = false) + var reservation: LiveReservation? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancelRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancelRepository.kt new file mode 100644 index 0000000..e5eebe3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationCancelRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.live.reservation + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface LiveReservationCancelRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt index a0abd2c..ead7b72 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationController.kt @@ -4,9 +4,13 @@ import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.member.Member import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController @@ -21,4 +25,33 @@ class LiveReservationController(private val service: LiveReservationService) { ApiResponse.ok(service.makeReservation(request, member.id!!)) } + + @GetMapping + fun getReservationList( + @RequestParam isActive: Boolean, + @RequestParam(value = "timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.getReservationList(member.id!!, isActive, timezone)) + } + + @GetMapping("/{id}") + fun getReservation( + @PathVariable id: Long, + @RequestParam(value = "timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.getReservation(id, member.id!!, timezone)) + } + + @PutMapping("/cancel") + fun cancelReservation( + @RequestBody request: CancelLiveReservationRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.cancelReservation(request, member.id!!)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt index ecaf7d2..07a396f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationRepository.kt @@ -19,6 +19,12 @@ interface LiveReservationQueryRepository { fun getReservationBookerList(roomId: Long): List fun isExistsReservation(roomId: Long, memberId: Long): Boolean + fun getReservationListByMemberId(memberId: Long, active: Boolean): List + + fun getReservationByReservationAndMemberId( + reservationId: Long, + memberId: Long + ): LiveReservation? } @Repository @@ -67,4 +73,28 @@ class LiveReservationQueryRepositoryImpl(private val queryFactory: JPAQueryFacto ) .fetchFirst() != null } + + override fun getReservationListByMemberId(memberId: Long, active: Boolean): List { + return queryFactory + .selectFrom(liveReservation) + .innerJoin(liveReservation.member, member) + .innerJoin(liveReservation.room, liveRoom) + .where( + liveReservation.isActive.eq(active) + .and(member.id.eq(memberId)) + ) + .fetch() + } + + override fun getReservationByReservationAndMemberId(reservationId: Long, memberId: Long): LiveReservation? { + return queryFactory + .selectFrom(liveReservation) + .innerJoin(liveReservation.member, member) + .innerJoin(liveReservation.room, liveRoom) + .where( + liveReservation.id.eq(reservationId) + .and(member.id.eq(memberId)) + ) + .fetchFirst() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt index 9184f72..df2a75d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationService.kt @@ -6,8 +6,11 @@ import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.live.room.LiveRoomRepository import kr.co.vividnext.sodalive.live.room.LiveRoomType import kr.co.vividnext.sodalive.member.MemberRepository +import org.springframework.beans.factory.annotation.Value import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime import java.time.ZoneId import java.time.format.DateTimeFormatter @@ -16,7 +19,11 @@ class LiveReservationService( private val repository: LiveReservationRepository, private val liveRoomRepository: LiveRoomRepository, private val memberRepository: MemberRepository, - private val canPaymentService: CanPaymentService + private val canPaymentService: CanPaymentService, + private val liveReservationCancelRepository: LiveReservationCancelRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String ) { fun makeReservation(request: MakeLiveReservationRequest, memberId: Long): MakeLiveReservationResponse { val room = liveRoomRepository.findByIdOrNull(id = request.roomId) @@ -76,4 +83,99 @@ class LiveReservationService( remainingCan = haveCan - room.price ) } + + fun getReservationList(memberId: Long, active: Boolean, timezone: String): List { + return repository + .getReservationListByMemberId(memberId, active) + .asSequence() + .map { + val beginDateTime = it.room!!.beginDateTime + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetLiveReservationResponse( + reservationId = it.id!!, + roomId = it.room!!.id!!, + title = it.room!!.title, + coverImageUrl = if (it.room!!.coverImage != null) { + "$cloudFrontHost/${it.room!!.coverImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + price = it.room!!.price, + masterNickname = it.room!!.member!!.nickname, + beginDateTime = beginDateTime.format( + DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a") + ), + cancelable = beginDateTime.minusHours(4).isAfter( + LocalDateTime.now() + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + ) + ) + } + .toList() + } + + fun getReservation(reservationId: Long, memberId: Long, timezone: String): GetLiveReservationResponse { + val reservation = repository.getReservationByReservationAndMemberId(reservationId, memberId) + ?: throw SodaException("잘못된 예약정보 입니다.") + + val beginDateTime = reservation.room!!.beginDateTime + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + return GetLiveReservationResponse( + reservationId = reservation.id!!, + roomId = reservation.room!!.id!!, + title = reservation.room!!.title, + coverImageUrl = if (reservation.room!!.coverImage != null) { + "$cloudFrontHost/${reservation.room!!.coverImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + price = reservation.room!!.price, + masterNickname = reservation.room!!.member!!.nickname, + beginDateTime = beginDateTime.format( + DateTimeFormatter.ofPattern("yyyy.MM.dd E hh:mm a") + ), + cancelable = beginDateTime.minusHours(4).isAfter( + LocalDateTime.now() + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + ) + ) + } + + @Transactional + fun cancelReservation(request: CancelLiveReservationRequest, memberId: Long) { + if (request.reason.isBlank()) { + throw SodaException("취소사유를 입력하세요.") + } + + val reservation = repository.findByIdOrNull(request.reservationId) + ?: throw SodaException("잘못된 예약정보 입니다.") + + if (reservation.member == null || reservation.member!!.id!! != memberId) { + throw SodaException("잘못된 예약정보 입니다.") + } + + if (reservation.room == null || reservation.room?.id == null) { + throw SodaException("잘못된 예약정보 입니다.") + } + + if (reservation.room!!.beginDateTime.isBefore(LocalDateTime.now().plusHours(4))) { + throw SodaException("라이브 시작 4시간 이내에는 예약취소가 불가능 합니다.") + } + + if (reservation.room!!.price > 0) { + canPaymentService.refund(memberId, roomId = reservation.room!!.id!!) + } + + reservation.isActive = false + + val reservationCancel = LiveReservationCancel(request.reason) + reservationCancel.reservation = reservation + liveReservationCancelRepository.save(reservationCancel) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt index 711371b..f060fe9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt @@ -381,7 +381,7 @@ class LiveRoomService( it.status = UseCanCalculateStatus.REFUND val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) - charge.title = "${it.can} 코인" + charge.title = "${it.can} 캔" charge.useCan = useCan when (it.paymentGateway) { @@ -823,20 +823,20 @@ class LiveRoomService( @Transactional fun refundDonation(roomId: Long, member: Member) { val donator = memberRepository.findByIdOrNull(member.id) - ?: throw SodaException("후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요.") + ?: throw SodaException("후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요.") val useCan = canRepository.getCanUsedForLiveRoomNotRefund( memberId = member.id!!, roomId = roomId, canUsage = CanUsage.DONATION - ) ?: throw SodaException("후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요.") + ) ?: throw SodaException("후원에 실패한 캔이 환불되지 않았습니다\n고객센터로 문의해주세요.") useCan.isRefund = true val useCoinCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!) useCoinCalculates.forEach { it.status = UseCanCalculateStatus.REFUND val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) - charge.title = "${it.can} 코인" + charge.title = "${it.can} 캔" charge.useCan = useCan when (it.paymentGateway) {