feat(gift): 관리자 선물 취소와 발신자 환불을 추가한다

This commit is contained in:
2026-10-22 18:23:18 +09:00
parent 6a9e76c13d
commit 710b49c175
2 changed files with 60 additions and 2 deletions
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.gift.application package kr.co.vividnext.sodalive.v2.gift.application
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategory import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategory
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
@@ -73,7 +74,8 @@ class GiftAdminService(
private val giftRepository: GiftRepository, private val giftRepository: GiftRepository,
private val giftDeliveryRepository: GiftDeliveryRepository, private val giftDeliveryRepository: GiftDeliveryRepository,
private val giftMailboxRepository: GiftMailboxRepository, private val giftMailboxRepository: GiftMailboxRepository,
private val giftPushService: GiftPushService private val giftPushService: GiftPushService,
private val canPaymentService: CanPaymentService
) { ) {
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getMailbox(): GiftMailboxResult? = giftMailboxRepository.findByIdOrNull(GiftMailbox.SINGLETON_ID)?.toResult() fun getMailbox(): GiftMailboxResult? = giftMailboxRepository.findByIdOrNull(GiftMailbox.SINGLETON_ID)?.toResult()
@@ -150,6 +152,19 @@ class GiftAdminService(
return sizePriceRepository.save(sizePrice).toResult() return sizePriceRepository.save(sizePrice).toResult()
} }
@Transactional
fun cancelGift(applicationNo: String, now: LocalDateTime = LocalDateTime.now()): GiftOperationStatusResult {
val gift = giftRepository.findByApplicationNoForUpdate(applicationNo) ?: throw invalidRequest()
if (gift.status != GiftStatus.RECEIVED) throw invalidRequest()
val delivery = giftDeliveryRepository.findByGiftId(requireNotNull(gift.id)) ?: throw invalidRequest()
gift.status = GiftStatus.CANCELED
delivery.canceledAt = now
canPaymentService.refundGiftCan(gift.senderMemberId, gift.canUsageId)
return GiftOperationStatusResult(gift.applicationNo, gift.status, gift.status.displayName, now)
}
@Transactional @Transactional
fun arriveMailbox(applicationNo: String, now: LocalDateTime = LocalDateTime.now()): GiftOperationStatusResult { fun arriveMailbox(applicationNo: String, now: LocalDateTime = LocalDateTime.now()): GiftOperationStatusResult {
val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest() val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest()
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.gift.application package kr.co.vividnext.sodalive.v2.gift.application
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.configs.QueryDslConfig import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.Gift import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.Gift
@@ -17,6 +18,7 @@ import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.EnumSource
import org.junit.jupiter.params.provider.ValueSource import org.junit.jupiter.params.provider.ValueSource
import org.mockito.Mockito import org.mockito.Mockito
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
@@ -40,13 +42,15 @@ class GiftAdminServiceTest @Autowired constructor(
private val giftMailboxRepository: GiftMailboxRepository private val giftMailboxRepository: GiftMailboxRepository
) { ) {
private val giftPushService = Mockito.mock(GiftPushService::class.java) private val giftPushService = Mockito.mock(GiftPushService::class.java)
private val canPaymentService = Mockito.mock(CanPaymentService::class.java)
private val service = GiftAdminService( private val service = GiftAdminService(
categoryRepository, categoryRepository,
sizePriceRepository, sizePriceRepository,
giftRepository, giftRepository,
giftDeliveryRepository, giftDeliveryRepository,
giftMailboxRepository, giftMailboxRepository,
giftPushService giftPushService,
canPaymentService
) )
@Test @Test
@@ -312,6 +316,45 @@ class GiftAdminServiceTest @Autowired constructor(
Mockito.verifyNoInteractions(giftPushService) Mockito.verifyNoInteractions(giftPushService)
} }
@Test
@DisplayName("운영자는 접수 선물을 취소하고 원 발신자의 결제 내역을 한 번 환불한다")
fun shouldCancelReceivedGiftWithOriginalSenderRefund() {
val (gift, delivery) = saveGift(GiftStatus.RECEIVED)
val result = service.cancelGift(APPLICATION_NO, NOW)
assertEquals(GiftOperationStatusResult(APPLICATION_NO, GiftStatus.CANCELED, "신청 취소", NOW), result)
assertEquals(GiftStatus.CANCELED, gift.status)
assertEquals(NOW, delivery.canceledAt)
assertThrows(SodaException::class.java) { service.cancelGift(APPLICATION_NO, NOW.plusSeconds(1)) }
assertEquals(NOW, delivery.canceledAt)
Mockito.verify(canPaymentService, Mockito.times(1)).refundGiftCan(SENDER_MEMBER_ID, 10L)
Mockito.verifyNoInteractions(giftPushService)
}
@ParameterizedTest
@EnumSource(value = GiftStatus::class, names = ["RECEIVED"], mode = EnumSource.Mode.EXCLUDE)
@DisplayName("운영자는 접수가 아닌 모든 상태의 선물을 취소할 수 없다")
fun shouldRejectCancellationForEveryNonReceivedStatus(status: GiftStatus) {
val (gift, delivery) = saveGift(status)
val exception = assertThrows(SodaException::class.java) { service.cancelGift(APPLICATION_NO, NOW) }
assertEquals("common.error.invalid_request", exception.messageKey)
assertEquals(status, gift.status)
assertEquals(null, delivery.canceledAt)
Mockito.verifyNoInteractions(canPaymentService, giftPushService)
}
@Test
@DisplayName("운영자는 없는 선물을 취소할 수 없다")
fun shouldRejectCancellationForMissingGift() {
val exception = assertThrows(SodaException::class.java) { service.cancelGift("missing", NOW) }
assertEquals("common.error.invalid_request", exception.messageKey)
Mockito.verifyNoInteractions(canPaymentService, giftPushService)
}
private fun populateRecipientInformation(delivery: GiftDelivery) { private fun populateRecipientInformation(delivery: GiftDelivery) {
delivery.recipientName = "김소다" delivery.recipientName = "김소다"
delivery.recipientPhoneNumber = "01098765432" delivery.recipientPhoneNumber = "01098765432"