diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt index 88d49ebc..ba8be075 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftCommandService.kt @@ -18,6 +18,7 @@ import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime +import java.time.temporal.ChronoUnit data class GiftApplicationCommand( val recipientMemberId: Long, @@ -297,7 +298,7 @@ class GiftCommandService( throw invalidRequest() } val delivery = deliveryRepository.findByGiftIdForUpdate(gift.id ?: throw invalidRequest()) ?: throw invalidRequest() - val deliveredAt = now() + val deliveredAt = now().truncatedTo(ChronoUnit.SECONDS) gift.status = GiftStatus.DELIVERED delivery.deliveredAt = deliveredAt diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt index 2eac4430..8768e9d1 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/gift/adapter/in/web/AdminGiftControllerTest.kt @@ -22,6 +22,7 @@ import kr.co.vividnext.sodalive.member.MemberAdapter import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer +import kr.co.vividnext.sodalive.v2.api.gift.dto.GiftDeliveryConfirmationResponse import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.Gift import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategory import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository @@ -29,11 +30,13 @@ 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.GiftReview +import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftReviewRepository import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePrice import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository import kr.co.vividnext.sodalive.v2.gift.adapter.out.scheduler.GiftScheduler import kr.co.vividnext.sodalive.v2.gift.application.GiftAdminService import kr.co.vividnext.sodalive.v2.gift.application.GiftApplicationCommand +import kr.co.vividnext.sodalive.v2.gift.application.GiftApplicationNoGenerator import kr.co.vividnext.sodalive.v2.gift.application.GiftCommandService import kr.co.vividnext.sodalive.v2.gift.application.GiftPushService import kr.co.vividnext.sodalive.v2.gift.application.GiftRecipientAddressRegistrationCommand @@ -55,6 +58,7 @@ 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.MethodSource +import org.junit.jupiter.params.provider.ValueSource import org.mockito.Mockito import org.springframework.aop.framework.Advised import org.springframework.aop.support.AopUtils @@ -121,6 +125,8 @@ class AdminGiftControllerTest @Autowired constructor( private val sizePriceRepository: GiftSizePriceRepository, private val giftRepository: GiftRepository, private val giftDeliveryRepository: GiftDeliveryRepository, + private val giftReviewRepository: GiftReviewRepository, + private val applicationNoGenerator: GiftApplicationNoGenerator, private val memberRepository: MemberRepository, private val chargeRepository: ChargeRepository, private val useCanRepository: UseCanRepository, @@ -592,6 +598,53 @@ class AdminGiftControllerTest @Autowired constructor( println("P16_CONFIRM_HTTP status=200 success=true rejected=true repeated=false senderPush=1 recipientPush=0") } + @ParameterizedTest + @ValueSource(ints = [0, 123456789, 999999999]) + @DisplayName("수령 확인 시각은 나노초 입력에도 초 단위 DB 저장값과 응답이 일치한다") + fun shouldPersistAndReturnSameSecondPrecisionDeliveryTime(nanos: Int) { + val (gift, _) = saveAdminGift( + applicationNo = APPLICATION_NO, + status = GiftStatus.INSPECTION_COMPLETED, + senderNickname = "precision-sender", + recipientNickname = "precision-recipient", + withTracking = true + ) + val giftId = requireNotNull(gift.id) + val fixedNow = LocalDateTime.of(2026, 10, 22, 23, 59, 59, nanos) + val service = GiftCommandService( + giftRepository = giftRepository, + deliveryRepository = giftDeliveryRepository, + reviewRepository = giftReviewRepository, + categoryRepository = categoryRepository, + sizePriceRepository = sizePriceRepository, + memberRepository = memberRepository, + applicationNoGenerator = applicationNoGenerator, + canPaymentService = canPaymentService, + giftPushService = giftPushService, + now = { fixedNow } + ) + + val result = requireNotNull( + TransactionTemplate(transactionManager).execute { + val recipient = memberRepository.findById(gift.recipientMemberId).orElseThrow() + service.confirmDelivery(recipient, gift.applicationNo).also { + entityManager.flush() + entityManager.clear() + } + } + ) + val persisted = requireNotNull(giftDeliveryRepository.findByGiftId(giftId)?.deliveredAt) + println( + "P16_CONFIRM_PRECISION nanos=$nanos input=$fixedNow returned=${result.deliveredAt} " + + "reloaded=$persisted expected=2026-10-22T23:59:59" + ) + assertEquals(persisted, result.deliveredAt) + assertEquals(LocalDateTime.of(2026, 10, 22, 23, 59, 59), persisted) + assertEquals(0, persisted.nano) + assertEquals("2026-10-22T23:59:59Z", GiftDeliveryConfirmationResponse.from(result).deliveredAt) + assertEquals(GiftStatus.DELIVERED, giftRepository.findById(giftId).orElseThrow().status) + } + @Test @DisplayName("실제 transactional 스케줄러 배송지 만료는 발신자만 한 번 알리고 재실행은 추가 알림을 보내지 않는다") fun shouldExpireRecipientAddressThroughTransactionalSchedulerAndNotifyOnlySenderOnce() {