feat(gift): 선물 관리자 서비스를 추가한다

This commit is contained in:
2026-09-30 13:37:44 +09:00
parent fcb1c1e8d1
commit 6bf4a682bf
2 changed files with 433 additions and 0 deletions
@@ -0,0 +1,223 @@
package kr.co.vividnext.sodalive.v2.gift.application
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
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDelivery
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftDeliveryRepository
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftRepository
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
import kr.co.vividnext.sodalive.v2.gift.domain.GiftSize
import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import java.time.LocalDateTime
@DataJpaTest(
properties = [
"spring.cache.type=none",
"spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;NON_KEYWORDS=VALUE"
]
)
@Import(QueryDslConfig::class)
@DisplayName("선물 관리자 서비스 테스트")
class GiftAdminServiceTest @Autowired constructor(
private val categoryRepository: GiftCategoryRepository,
private val sizePriceRepository: GiftSizePriceRepository,
private val giftRepository: GiftRepository,
private val giftDeliveryRepository: GiftDeliveryRepository
) {
private val giftPushService = Mockito.mock(GiftPushService::class.java)
private val service = GiftAdminService(
categoryRepository,
sizePriceRepository,
giftRepository,
giftDeliveryRepository,
giftPushService
)
@Test
@DisplayName("카테고리를 등록, 수정, 논리삭제한다")
fun shouldCreateUpdateAndDeactivateCategory() {
val created = service.createCategory(
GiftCategoryCommand(categoryCode = "DOLL", name = "인형", requiresDamageWaiver = true, isActive = true)
)
val updated = service.updateCategory(
created.categoryId,
GiftCategoryCommand(categoryCode = "DOLL", name = "큰 인형", requiresDamageWaiver = false, isActive = true)
)
service.deleteCategory(created.categoryId)
val saved = categoryRepository.findById(created.categoryId).orElseThrow()
assertEquals("큰 인형", updated.name)
assertFalse(updated.requiresDamageWaiver)
assertFalse(saved.isActive)
}
@Test
@DisplayName("사이즈 가격을 수정하고 salePriceCan이 basePriceCan보다 크면 거부한다")
fun shouldUpdateSizePriceAndRejectInvalidSalePrice() {
val updated = service.updateSizePrice(
GiftSize.SMALL,
GiftSizePriceCommand(basePriceCan = 100, salePriceCan = 80, isActive = true)
)
val exception = assertThrows(SodaException::class.java) {
service.updateSizePrice(
GiftSize.SMALL,
GiftSizePriceCommand(basePriceCan = 100, salePriceCan = 101, isActive = true)
)
}
assertEquals(GiftSize.SMALL, updated.sizeCode)
assertEquals(80, updated.salePriceCan)
assertEquals("common.error.invalid_request", exception.messageKey)
}
@Test
@DisplayName("운영자는 발송 확인 선물을 사서함 도착 처리한다")
fun shouldMarkTrackingRegisteredGiftArrivedAtMailbox() {
val (gift, delivery) = saveGift(GiftStatus.TRACKING_REGISTERED)
val result = service.arriveMailbox(APPLICATION_NO, NOW)
assertEquals(GiftStatus.ARRIVED_AT_MAILBOX, result.status)
assertEquals(GiftStatus.ARRIVED_AT_MAILBOX, gift.status)
assertEquals(NOW, delivery.arrivedAtMailboxAt)
Mockito.verify(giftPushService).sendArrivedAtMailboxToSender(APPLICATION_NO, SENDER_MEMBER_ID)
Mockito.verify(giftPushService).sendArrivedAtMailboxToRecipient(APPLICATION_NO, RECIPIENT_MEMBER_ID)
}
@Test
@DisplayName("사서함 도착 상태가 아니면 검수 완료 처리할 수 없다")
fun shouldRejectInspectionCompletionWhenGiftIsNotArrivedAtMailbox() {
saveGift(GiftStatus.TRACKING_REGISTERED)
val exception = assertThrows(SodaException::class.java) {
service.completeInspection(APPLICATION_NO, NOW)
}
assertEquals("common.error.invalid_request", exception.messageKey)
}
@Test
@DisplayName("운영자는 사서함 도착 선물을 검수 완료 처리한다")
fun shouldCompleteInspectionForArrivedAtMailboxGift() {
val (gift, delivery) = saveGift(GiftStatus.ARRIVED_AT_MAILBOX)
val result = service.completeInspection(APPLICATION_NO, NOW)
assertEquals(GiftStatus.INSPECTION_COMPLETED, result.status)
assertEquals(GiftStatus.INSPECTION_COMPLETED, gift.status)
assertEquals(NOW, delivery.inspectionCompletedAt)
Mockito.verify(giftPushService).sendInspectionCompleted(APPLICATION_NO, RECIPIENT_MEMBER_ID)
}
@Test
@DisplayName("운영자는 진행 중 선물을 전달 불가 처리한다")
fun shouldMarkActiveGiftUndeliverable() {
val (gift, delivery) = saveGift(GiftStatus.INSPECTION_COMPLETED)
val result = service.markUndeliverable(APPLICATION_NO, UNDELIVERABLE_REASON, NOW)
assertEquals(GiftStatus.UNDELIVERABLE, result.status)
assertEquals(GiftStatus.UNDELIVERABLE, gift.status)
assertEquals(NOW, delivery.undeliverableAt)
assertEquals(UNDELIVERABLE_REASON, delivery.undeliverableReason)
Mockito.verify(giftPushService).sendUndeliverableToSender(APPLICATION_NO, SENDER_MEMBER_ID)
Mockito.verify(giftPushService).sendUndeliverableToRecipient(APPLICATION_NO, RECIPIENT_MEMBER_ID)
}
@Test
@DisplayName("전달 불가 사유가 비어 있거나 종료 상태면 전달 불가 처리할 수 없다")
fun shouldRejectUndeliverableWhenReasonBlankOrGiftTerminated() {
saveGift(GiftStatus.DELIVERED)
val blankReasonException = assertThrows(SodaException::class.java) {
service.markUndeliverable(APPLICATION_NO, " ", NOW)
}
val terminatedStatusException = assertThrows(SodaException::class.java) {
service.markUndeliverable(APPLICATION_NO, UNDELIVERABLE_REASON, NOW)
}
assertEquals("common.error.invalid_request", blankReasonException.messageKey)
assertEquals("common.error.invalid_request", terminatedStatusException.messageKey)
}
@Test
@DisplayName("운영자는 검수 완료 선물을 전달 완료 처리한다")
fun shouldCompleteDeliveryForInspectionCompletedGift() {
val (gift, delivery) = saveGift(GiftStatus.INSPECTION_COMPLETED)
val result = service.completeDelivery(APPLICATION_NO, NOW)
assertEquals(GiftStatus.DELIVERED, result.status)
assertEquals(GiftStatus.DELIVERED, gift.status)
assertEquals(NOW, delivery.deliveredAt)
Mockito.verify(giftPushService).sendDeliveredToSender(APPLICATION_NO, SENDER_MEMBER_ID)
Mockito.verify(giftPushService).sendDeliveredToRecipient(APPLICATION_NO, RECIPIENT_MEMBER_ID)
Mockito.verifyNoMoreInteractions(giftPushService)
}
@Test
@DisplayName("검수 완료 상태가 아니면 운영 전달 완료 처리할 수 없다")
fun shouldRejectDeliveryCompletionWhenGiftIsNotInspectionCompleted() {
saveGift(GiftStatus.ARRIVED_AT_MAILBOX)
val exception = assertThrows(SodaException::class.java) {
service.completeDelivery(APPLICATION_NO, NOW)
}
assertEquals("common.error.invalid_request", exception.messageKey)
}
private fun saveGift(status: GiftStatus): Pair<Gift, GiftDelivery> {
val gift = giftRepository.save(
Gift(
applicationNo = APPLICATION_NO,
senderMemberId = SENDER_MEMBER_ID,
recipientMemberId = RECIPIENT_MEMBER_ID,
status = status,
sizeCode = GiftSize.SMALL,
categoryId = 1L,
categoryNameSnapshot = "인형",
salePriceCan = 80,
canUsageId = 10L,
senderTermsAgreed = true,
senderPrivacyAgreed = true,
senderTermsAgreedAt = NOW.minusDays(1),
senderPrivacyAgreedAt = NOW.minusDays(1),
damageWaiverAgreed = true
)
)
val delivery = giftDeliveryRepository.save(
GiftDelivery(
giftId = requireNotNull(gift.id),
senderName = "홍길동",
senderPhoneNumber = "01012345678",
senderZipCode = "06234",
senderAddress = "서울시 강남구",
trackingDeadlineAt = NOW.minusDays(1),
trackingRegisteredAt = NOW.minusHours(1)
)
)
return gift to delivery
}
private companion object {
const val APPLICATION_NO = "G20260929000001"
const val SENDER_MEMBER_ID = 1L
const val RECIPIENT_MEMBER_ID = 2L
const val UNDELIVERABLE_REASON = "선물 접수 규격에 맞지 않습니다."
val NOW: LocalDateTime = LocalDateTime.of(2026, 10, 3, 12, 0)
}
}