From 710b49c17540a88fc9f40ee542a7731f2311c3f2 Mon Sep 17 00:00:00 2001 From: Klaus Date: Thu, 22 Oct 2026 18:23:18 +0900 Subject: [PATCH] =?UTF-8?q?feat(gift):=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EC=84=A0=EB=AC=BC=20=EC=B7=A8=EC=86=8C=EC=99=80=20=EB=B0=9C?= =?UTF-8?q?=EC=8B=A0=EC=9E=90=20=ED=99=98=EB=B6=88=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v2/gift/application/GiftAdminService.kt | 17 ++++++- .../gift/application/GiftAdminServiceTest.kt | 45 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminService.kt index a4ba308d..64e7eaa9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminService.kt @@ -1,5 +1,6 @@ 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.v2.gift.adapter.out.persistence.GiftCategory import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository @@ -73,7 +74,8 @@ class GiftAdminService( private val giftRepository: GiftRepository, private val giftDeliveryRepository: GiftDeliveryRepository, private val giftMailboxRepository: GiftMailboxRepository, - private val giftPushService: GiftPushService + private val giftPushService: GiftPushService, + private val canPaymentService: CanPaymentService ) { @Transactional(readOnly = true) fun getMailbox(): GiftMailboxResult? = giftMailboxRepository.findByIdOrNull(GiftMailbox.SINGLETON_ID)?.toResult() @@ -150,6 +152,19 @@ class GiftAdminService( 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 fun arriveMailbox(applicationNo: String, now: LocalDateTime = LocalDateTime.now()): GiftOperationStatusResult { val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest() diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminServiceTest.kt index 86610855..447c2631 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftAdminServiceTest.kt @@ -1,5 +1,6 @@ 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.configs.QueryDslConfig 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.Test import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource import org.junit.jupiter.params.provider.ValueSource import org.mockito.Mockito import org.springframework.beans.factory.annotation.Autowired @@ -40,13 +42,15 @@ class GiftAdminServiceTest @Autowired constructor( private val giftMailboxRepository: GiftMailboxRepository ) { private val giftPushService = Mockito.mock(GiftPushService::class.java) + private val canPaymentService = Mockito.mock(CanPaymentService::class.java) private val service = GiftAdminService( categoryRepository, sizePriceRepository, giftRepository, giftDeliveryRepository, giftMailboxRepository, - giftPushService + giftPushService, + canPaymentService ) @Test @@ -312,6 +316,45 @@ class GiftAdminServiceTest @Autowired constructor( 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) { delivery.recipientName = "김소다" delivery.recipientPhoneNumber = "01098765432"