라이브 예약 취소 API

This commit is contained in:
Klaus 2023-08-02 19:11:25 +09:00
parent 980faae943
commit 25b3bcb534
9 changed files with 249 additions and 5 deletions

View File

@ -1,6 +1,9 @@
package kr.co.vividnext.sodalive.can.payment 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.ChargeRepository
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.SpentCan import kr.co.vividnext.sodalive.can.use.SpentCan
import kr.co.vividnext.sodalive.can.use.TotalSpentCan import kr.co.vividnext.sodalive.can.use.TotalSpentCan
@ -18,6 +21,7 @@ import org.springframework.transaction.annotation.Transactional
@Service @Service
class CanPaymentService( class CanPaymentService(
private val repository: CanRepository,
private val memberRepository: MemberRepository, private val memberRepository: MemberRepository,
private val chargeRepository: ChargeRepository, private val chargeRepository: ChargeRepository,
private val useCanRepository: UseCanRepository, private val useCanRepository: UseCanRepository,
@ -242,4 +246,41 @@ class CanPaymentService(
TotalSpentCan(total = 0) 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)
}
}
} }

View File

@ -0,0 +1,3 @@
package kr.co.vividnext.sodalive.live.reservation
data class CancelLiveReservationRequest(val reservationId: Long, val reason: String)

View File

@ -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
)

View File

@ -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
}

View File

@ -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<LiveReservationCancel, Long>

View File

@ -4,9 +4,13 @@ import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.Member
import org.springframework.security.core.annotation.AuthenticationPrincipal 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.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
@RestController @RestController
@ -21,4 +25,33 @@ class LiveReservationController(private val service: LiveReservationService) {
ApiResponse.ok(service.makeReservation(request, member.id!!)) 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!!))
}
} }

View File

@ -19,6 +19,12 @@ interface LiveReservationQueryRepository {
fun getReservationBookerList(roomId: Long): List<Member> fun getReservationBookerList(roomId: Long): List<Member>
fun isExistsReservation(roomId: Long, memberId: Long): Boolean fun isExistsReservation(roomId: Long, memberId: Long): Boolean
fun getReservationListByMemberId(memberId: Long, active: Boolean): List<LiveReservation>
fun getReservationByReservationAndMemberId(
reservationId: Long,
memberId: Long
): LiveReservation?
} }
@Repository @Repository
@ -67,4 +73,28 @@ class LiveReservationQueryRepositoryImpl(private val queryFactory: JPAQueryFacto
) )
.fetchFirst() != null .fetchFirst() != null
} }
override fun getReservationListByMemberId(memberId: Long, active: Boolean): List<LiveReservation> {
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()
}
} }

View File

@ -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.LiveRoomRepository
import kr.co.vividnext.sodalive.live.room.LiveRoomType import kr.co.vividnext.sodalive.live.room.LiveRoomType
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
import java.time.ZoneId import java.time.ZoneId
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
@ -16,7 +19,11 @@ class LiveReservationService(
private val repository: LiveReservationRepository, private val repository: LiveReservationRepository,
private val liveRoomRepository: LiveRoomRepository, private val liveRoomRepository: LiveRoomRepository,
private val memberRepository: MemberRepository, 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 { fun makeReservation(request: MakeLiveReservationRequest, memberId: Long): MakeLiveReservationResponse {
val room = liveRoomRepository.findByIdOrNull(id = request.roomId) val room = liveRoomRepository.findByIdOrNull(id = request.roomId)
@ -76,4 +83,99 @@ class LiveReservationService(
remainingCan = haveCan - room.price remainingCan = haveCan - room.price
) )
} }
fun getReservationList(memberId: Long, active: Boolean, timezone: String): List<GetLiveReservationResponse> {
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)
}
} }

View File

@ -381,7 +381,7 @@ class LiveRoomService(
it.status = UseCanCalculateStatus.REFUND it.status = UseCanCalculateStatus.REFUND
val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE)
charge.title = "${it.can} 코인" charge.title = "${it.can} "
charge.useCan = useCan charge.useCan = useCan
when (it.paymentGateway) { when (it.paymentGateway) {
@ -823,20 +823,20 @@ class LiveRoomService(
@Transactional @Transactional
fun refundDonation(roomId: Long, member: Member) { fun refundDonation(roomId: Long, member: Member) {
val donator = memberRepository.findByIdOrNull(member.id) val donator = memberRepository.findByIdOrNull(member.id)
?: throw SodaException("후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요.") ?: throw SodaException("후원에 실패한 이 환불되지 않았습니다\n고객센터로 문의해주세요.")
val useCan = canRepository.getCanUsedForLiveRoomNotRefund( val useCan = canRepository.getCanUsedForLiveRoomNotRefund(
memberId = member.id!!, memberId = member.id!!,
roomId = roomId, roomId = roomId,
canUsage = CanUsage.DONATION canUsage = CanUsage.DONATION
) ?: throw SodaException("후원에 실패한 코인이 환불되지 않았습니다\n고객센터로 문의해주세요.") ) ?: throw SodaException("후원에 실패한 이 환불되지 않았습니다\n고객센터로 문의해주세요.")
useCan.isRefund = true useCan.isRefund = true
val useCoinCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!) val useCoinCalculates = useCanCalculateRepository.findByUseCanIdAndStatus(useCan.id!!)
useCoinCalculates.forEach { useCoinCalculates.forEach {
it.status = UseCanCalculateStatus.REFUND it.status = UseCanCalculateStatus.REFUND
val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE) val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE)
charge.title = "${it.can} 코인" charge.title = "${it.can} "
charge.useCan = useCan charge.useCan = useCan
when (it.paymentGateway) { when (it.paymentGateway) {