feat(gift): 선물 조회 서비스를 추가한다
This commit is contained in:
@@ -0,0 +1,259 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.gift.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
|
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.GiftDirection
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.domain.GiftListType
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.domain.GiftStatus
|
||||||
|
import org.springframework.data.domain.PageRequest
|
||||||
|
import org.springframework.data.domain.Sort
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
data class GiftFormOptionsResult(
|
||||||
|
val sizes: List<GiftSizePriceResult>,
|
||||||
|
val categories: List<GiftCategoryResult>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GiftListResult(
|
||||||
|
val totalCount: Long,
|
||||||
|
val items: List<GiftListItemResult>,
|
||||||
|
val page: Int,
|
||||||
|
val size: Int,
|
||||||
|
val hasNext: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GiftListItemResult(
|
||||||
|
val applicationNo: String,
|
||||||
|
val direction: GiftDirection,
|
||||||
|
val status: GiftStatus,
|
||||||
|
val statusName: String,
|
||||||
|
val priceCan: Int,
|
||||||
|
val categoryName: String,
|
||||||
|
val sizeName: String,
|
||||||
|
val createdAt: LocalDateTime?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GiftDetailResult(
|
||||||
|
val applicationNo: String,
|
||||||
|
val direction: GiftDirection,
|
||||||
|
val status: GiftStatus,
|
||||||
|
val statusName: String,
|
||||||
|
val giftInfo: GiftDetailInfoResult,
|
||||||
|
val senderInfo: GiftAddressResult?,
|
||||||
|
val recipientAddress: GiftAddressResult?,
|
||||||
|
val mailbox: GiftMailboxResult?,
|
||||||
|
val trackingRequired: Boolean,
|
||||||
|
val recipientAddressRequired: Boolean,
|
||||||
|
val recipientAddressDeadlineAt: LocalDateTime?,
|
||||||
|
val delivery: GiftDeliveryInfoResult,
|
||||||
|
val statusTimeline: List<GiftStatusTimelineResult>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GiftDetailInfoResult(
|
||||||
|
val recipientCreatorNickname: String?,
|
||||||
|
val senderNickname: String?,
|
||||||
|
val sizeName: String,
|
||||||
|
val categoryName: String,
|
||||||
|
val applicationNo: String?,
|
||||||
|
val paidCan: Int?,
|
||||||
|
val tracking: String?,
|
||||||
|
val shippingRequestedAt: LocalDateTime?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GiftAddressResult(
|
||||||
|
val name: String,
|
||||||
|
val phoneNumber: String,
|
||||||
|
val address: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GiftMailboxResult(
|
||||||
|
val name: String,
|
||||||
|
val address: String,
|
||||||
|
val phoneNumber: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GiftDeliveryInfoResult(
|
||||||
|
val canceledAt: LocalDateTime?,
|
||||||
|
val undeliverableAt: LocalDateTime?,
|
||||||
|
val undeliverableReason: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GiftStatusTimelineResult(
|
||||||
|
val status: GiftStatus,
|
||||||
|
val statusName: String,
|
||||||
|
val occurredAt: LocalDateTime?
|
||||||
|
)
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class GiftQueryService(
|
||||||
|
private val categoryRepository: GiftCategoryRepository,
|
||||||
|
private val sizePriceRepository: GiftSizePriceRepository,
|
||||||
|
private val giftRepository: GiftRepository,
|
||||||
|
private val deliveryRepository: GiftDeliveryRepository,
|
||||||
|
private val memberRepository: MemberRepository
|
||||||
|
) {
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun getFormOptions() = GiftFormOptionsResult(
|
||||||
|
sizes = sizePriceRepository.findAll().filter { it.isActive }.map {
|
||||||
|
GiftSizePriceResult(it.sizeCode, it.sizeCode.displayName, it.basePriceCan, it.salePriceCan, it.isActive)
|
||||||
|
},
|
||||||
|
categories = categoryRepository.findAll().filter { it.isActive }.map {
|
||||||
|
GiftCategoryResult(it.id!!, it.categoryCode, it.name, it.requiresDamageWaiver, it.isActive)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun getGifts(memberId: Long, type: GiftListType, page: Int, size: Int): GiftListResult {
|
||||||
|
val normalizedPage = page.coerceAtLeast(0)
|
||||||
|
val normalizedSize = if (size <= 0) DEFAULT_PAGE_SIZE else size
|
||||||
|
val pageable = PageRequest.of(normalizedPage, normalizedSize, Sort.by(Sort.Direction.DESC, "createdAt", "id"))
|
||||||
|
val gifts = when (type) {
|
||||||
|
GiftListType.SENT -> giftRepository.findBySenderMemberId(memberId, pageable)
|
||||||
|
GiftListType.RECEIVED -> giftRepository.findByRecipientMemberIdAndStatusIn(
|
||||||
|
memberId,
|
||||||
|
RECIPIENT_VISIBLE_STATUSES,
|
||||||
|
pageable
|
||||||
|
)
|
||||||
|
GiftListType.ALL -> giftRepository.findBySenderMemberIdOrRecipientMemberIdAndStatusIn(
|
||||||
|
memberId,
|
||||||
|
memberId,
|
||||||
|
RECIPIENT_VISIBLE_STATUSES,
|
||||||
|
pageable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return GiftListResult(
|
||||||
|
totalCount = gifts.totalElements,
|
||||||
|
items = gifts.content.map { it.toGiftListItem(memberId) },
|
||||||
|
page = normalizedPage,
|
||||||
|
size = normalizedSize,
|
||||||
|
hasNext = gifts.hasNext()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun getGiftDetail(memberId: Long, applicationNo: String): GiftDetailResult {
|
||||||
|
val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest()
|
||||||
|
val delivery = deliveryRepository.findByGiftId(gift.id ?: throw invalidRequest()) ?: throw invalidRequest()
|
||||||
|
val direction = gift.directionFor(memberId)
|
||||||
|
val sender = memberRepository.findByIdOrNull(gift.senderMemberId) ?: throw invalidRequest()
|
||||||
|
val recipient = memberRepository.findByIdOrNull(gift.recipientMemberId) ?: throw invalidRequest()
|
||||||
|
val recipientAddress = if (direction == GiftDirection.RECEIVED) delivery.toRecipientAddressResult() else null
|
||||||
|
|
||||||
|
return GiftDetailResult(
|
||||||
|
applicationNo = gift.applicationNo,
|
||||||
|
direction = direction,
|
||||||
|
status = gift.status,
|
||||||
|
statusName = gift.status.displayName,
|
||||||
|
giftInfo = GiftDetailInfoResult(
|
||||||
|
recipientCreatorNickname = recipient.nickname.takeIf { direction == GiftDirection.SENT },
|
||||||
|
senderNickname = sender.nickname.takeIf { direction == GiftDirection.RECEIVED },
|
||||||
|
sizeName = gift.sizeCode.displayName,
|
||||||
|
categoryName = gift.categoryNameSnapshot,
|
||||||
|
applicationNo = gift.applicationNo.takeIf { direction == GiftDirection.SENT },
|
||||||
|
paidCan = gift.salePriceCan.takeIf { direction == GiftDirection.SENT },
|
||||||
|
tracking = delivery.tracking().takeIf { direction == GiftDirection.SENT },
|
||||||
|
shippingRequestedAt = gift.createdAt.takeIf { direction == GiftDirection.RECEIVED }
|
||||||
|
),
|
||||||
|
senderInfo = delivery.toSenderInfo().takeIf { direction == GiftDirection.SENT },
|
||||||
|
recipientAddress = recipientAddress,
|
||||||
|
mailbox = null,
|
||||||
|
trackingRequired = direction == GiftDirection.SENT && gift.status == GiftStatus.RECEIVED,
|
||||||
|
recipientAddressRequired = direction == GiftDirection.RECEIVED && recipientAddress == null &&
|
||||||
|
gift.status !in TERMINAL_STATUSES,
|
||||||
|
recipientAddressDeadlineAt = delivery.recipientAddressDeadlineAt,
|
||||||
|
delivery = GiftDeliveryInfoResult(
|
||||||
|
canceledAt = delivery.canceledAt,
|
||||||
|
undeliverableAt = delivery.undeliverableAt,
|
||||||
|
undeliverableReason = delivery.undeliverableReason
|
||||||
|
),
|
||||||
|
statusTimeline = gift.toStatusTimeline(delivery)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Gift.toGiftListItem(memberId: Long) = GiftListItemResult(
|
||||||
|
applicationNo = applicationNo,
|
||||||
|
direction = if (senderMemberId == memberId) GiftDirection.SENT else GiftDirection.RECEIVED,
|
||||||
|
status = status,
|
||||||
|
statusName = status.displayName,
|
||||||
|
priceCan = salePriceCan,
|
||||||
|
categoryName = categoryNameSnapshot,
|
||||||
|
sizeName = sizeCode.displayName,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Gift.directionFor(memberId: Long) = when {
|
||||||
|
senderMemberId == memberId -> GiftDirection.SENT
|
||||||
|
recipientMemberId == memberId && status in RECIPIENT_VISIBLE_STATUSES -> GiftDirection.RECEIVED
|
||||||
|
else -> throw invalidRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GiftDelivery.toSenderInfo() = GiftAddressResult(
|
||||||
|
name = senderName,
|
||||||
|
phoneNumber = senderPhoneNumber,
|
||||||
|
address = formatAddress(senderZipCode, senderAddress, senderAddressDetail)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun GiftDelivery.toRecipientAddressResult(): GiftAddressResult? {
|
||||||
|
val name = recipientName ?: return null
|
||||||
|
val phoneNumber = recipientPhoneNumber ?: return null
|
||||||
|
val zipCode = recipientZipCode ?: return null
|
||||||
|
val address = recipientAddress ?: return null
|
||||||
|
|
||||||
|
return GiftAddressResult(name, phoneNumber, formatAddress(zipCode, address, recipientAddressDetail))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun GiftDelivery.tracking() = if (courierCompanyName != null && trackingNumber != null) {
|
||||||
|
"${courierCompanyName}_$trackingNumber"
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Gift.toStatusTimeline(delivery: GiftDelivery) = NORMAL_PROGRESS_STATUSES.map { timelineStatus ->
|
||||||
|
GiftStatusTimelineResult(
|
||||||
|
status = timelineStatus,
|
||||||
|
statusName = timelineStatus.displayName,
|
||||||
|
occurredAt = when (timelineStatus) {
|
||||||
|
GiftStatus.RECEIVED -> createdAt
|
||||||
|
GiftStatus.TRACKING_REGISTERED -> delivery.trackingRegisteredAt
|
||||||
|
GiftStatus.ARRIVED_AT_MAILBOX -> delivery.arrivedAtMailboxAt
|
||||||
|
GiftStatus.INSPECTION_COMPLETED -> delivery.inspectionCompletedAt
|
||||||
|
GiftStatus.DELIVERED -> delivery.deliveredAt
|
||||||
|
GiftStatus.UNDELIVERABLE, GiftStatus.CANCELED -> null
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatAddress(zipCode: String, address: String, detail: String?) =
|
||||||
|
"($zipCode) $address" + if (detail.isNullOrBlank()) "" else ", $detail"
|
||||||
|
|
||||||
|
private fun invalidRequest() = SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val DEFAULT_PAGE_SIZE = 20
|
||||||
|
val RECIPIENT_VISIBLE_STATUSES = setOf(
|
||||||
|
GiftStatus.TRACKING_REGISTERED,
|
||||||
|
GiftStatus.ARRIVED_AT_MAILBOX,
|
||||||
|
GiftStatus.INSPECTION_COMPLETED,
|
||||||
|
GiftStatus.DELIVERED,
|
||||||
|
GiftStatus.UNDELIVERABLE
|
||||||
|
)
|
||||||
|
val NORMAL_PROGRESS_STATUSES = listOf(
|
||||||
|
GiftStatus.RECEIVED,
|
||||||
|
GiftStatus.TRACKING_REGISTERED,
|
||||||
|
GiftStatus.ARRIVED_AT_MAILBOX,
|
||||||
|
GiftStatus.INSPECTION_COMPLETED,
|
||||||
|
GiftStatus.DELIVERED
|
||||||
|
)
|
||||||
|
val TERMINAL_STATUSES = setOf(GiftStatus.DELIVERED, GiftStatus.UNDELIVERABLE, GiftStatus.CANCELED)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
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.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
|
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
|
||||||
|
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.GiftSizePrice
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftSizePriceRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.domain.GiftDirection
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.domain.GiftListType
|
||||||
|
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.assertNotNull
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
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:gift-query;MODE=MySQL;NON_KEYWORDS=VALUE"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
@Import(QueryDslConfig::class)
|
||||||
|
class GiftQueryServiceTest @Autowired constructor(
|
||||||
|
private val categoryRepository: GiftCategoryRepository,
|
||||||
|
private val sizePriceRepository: GiftSizePriceRepository,
|
||||||
|
private val giftRepository: GiftRepository,
|
||||||
|
private val deliveryRepository: GiftDeliveryRepository,
|
||||||
|
private val memberRepository: MemberRepository
|
||||||
|
) {
|
||||||
|
@Test
|
||||||
|
fun `폼 옵션은 활성 카테고리와 활성 사이즈 가격만 반환한다`() {
|
||||||
|
val activeCategory = categoryRepository.save(GiftCategory("DOLL", "인형", true, true))
|
||||||
|
categoryRepository.save(GiftCategory("FOOD", "간식", false, false))
|
||||||
|
sizePriceRepository.save(GiftSizePrice(GiftSize.SMALL, 100, 80, true))
|
||||||
|
sizePriceRepository.save(GiftSizePrice(GiftSize.MEDIUM, 150, 120, false))
|
||||||
|
|
||||||
|
val result = queryService().getFormOptions()
|
||||||
|
|
||||||
|
assertEquals(listOf(GiftSize.SMALL), result.sizes.map { it.sizeCode })
|
||||||
|
assertEquals(listOf(100), result.sizes.map { it.basePriceCan })
|
||||||
|
assertEquals(listOf(80), result.sizes.map { it.salePriceCan })
|
||||||
|
assertEquals(listOf(activeCategory.id), result.categories.map { it.categoryId })
|
||||||
|
assertEquals("인형", result.categories.single().name)
|
||||||
|
assertEquals(true, result.categories.single().requiresDamageWaiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("보낸 선물은 접수 직후 선물함에 노출된다")
|
||||||
|
fun shouldListReceivedGiftForSender() {
|
||||||
|
saveGift("G-SENT-RECEIVED", senderMemberId = MEMBER_ID, recipientMemberId = 2L, status = GiftStatus.RECEIVED)
|
||||||
|
|
||||||
|
val result = queryService().getGifts(
|
||||||
|
memberId = MEMBER_ID,
|
||||||
|
type = GiftListType.SENT,
|
||||||
|
page = 0,
|
||||||
|
size = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(1L, result.totalCount)
|
||||||
|
assertEquals(1, result.items.size)
|
||||||
|
assertEquals("G-SENT-RECEIVED", result.items.single().applicationNo)
|
||||||
|
assertEquals(GiftDirection.SENT, result.items.single().direction)
|
||||||
|
assertEquals(GiftStatus.RECEIVED, result.items.single().status)
|
||||||
|
assertEquals(GiftStatus.RECEIVED.displayName, result.items.single().statusName)
|
||||||
|
assertEquals(80, result.items.single().priceCan)
|
||||||
|
assertEquals("인형", result.items.single().categoryName)
|
||||||
|
assertEquals("소형", result.items.single().sizeName)
|
||||||
|
assertNotNull(result.items.single().createdAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("받은 선물은 운송장 등록 이후 상태만 선물함에 노출된다")
|
||||||
|
fun shouldExcludeReceivedOnlyGiftAndListVisibleGiftForRecipient() {
|
||||||
|
saveGift("G-RECEIVED-HIDDEN", senderMemberId = 2L, recipientMemberId = MEMBER_ID, status = GiftStatus.RECEIVED)
|
||||||
|
saveGift(
|
||||||
|
"G-RECEIVED-VISIBLE",
|
||||||
|
senderMemberId = 3L,
|
||||||
|
recipientMemberId = MEMBER_ID,
|
||||||
|
status = GiftStatus.TRACKING_REGISTERED
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = queryService().getGifts(
|
||||||
|
memberId = MEMBER_ID,
|
||||||
|
type = GiftListType.RECEIVED,
|
||||||
|
page = 0,
|
||||||
|
size = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(1L, result.totalCount)
|
||||||
|
assertEquals(listOf("G-RECEIVED-VISIBLE"), result.items.map { it.applicationNo })
|
||||||
|
assertEquals(GiftDirection.RECEIVED, result.items.single().direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("전체 선물함은 보낸 선물과 노출 가능한 받은 선물을 방향과 함께 반환한다")
|
||||||
|
fun shouldListSentAndVisibleReceivedGiftsWithDirectionsForAll() {
|
||||||
|
saveGift("G-ALL-SENT", senderMemberId = MEMBER_ID, recipientMemberId = 2L, status = GiftStatus.CANCELED)
|
||||||
|
saveGift(
|
||||||
|
"G-ALL-RECEIVED",
|
||||||
|
senderMemberId = 3L,
|
||||||
|
recipientMemberId = MEMBER_ID,
|
||||||
|
status = GiftStatus.ARRIVED_AT_MAILBOX
|
||||||
|
)
|
||||||
|
saveGift("G-ALL-HIDDEN", senderMemberId = 4L, recipientMemberId = MEMBER_ID, status = GiftStatus.RECEIVED)
|
||||||
|
|
||||||
|
val result = queryService().getGifts(
|
||||||
|
memberId = MEMBER_ID,
|
||||||
|
type = GiftListType.ALL,
|
||||||
|
page = 0,
|
||||||
|
size = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(2L, result.totalCount)
|
||||||
|
assertEquals(
|
||||||
|
mapOf("G-ALL-SENT" to GiftDirection.SENT, "G-ALL-RECEIVED" to GiftDirection.RECEIVED),
|
||||||
|
result.items.associate { it.applicationNo to it.direction }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("선물함 page와 size는 음수 또는 0이면 기본값으로 보정된다")
|
||||||
|
fun shouldNormalizeGiftListPageAndSize() {
|
||||||
|
saveGift("G-PAGING", senderMemberId = MEMBER_ID, recipientMemberId = 2L, status = GiftStatus.RECEIVED)
|
||||||
|
|
||||||
|
val result = queryService().getGifts(
|
||||||
|
memberId = MEMBER_ID,
|
||||||
|
type = GiftListType.SENT,
|
||||||
|
page = -1,
|
||||||
|
size = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(0, result.page)
|
||||||
|
assertEquals(20, result.size)
|
||||||
|
assertEquals(false, result.hasNext)
|
||||||
|
assertEquals(listOf("G-PAGING"), result.items.map { it.applicationNo })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("발신자는 자신의 선물 상세와 발신자 정보만 조회한다")
|
||||||
|
fun shouldReturnSentGiftDetailWithoutRecipientAddress() {
|
||||||
|
val sender = saveMember("fan")
|
||||||
|
val recipient = saveMember("creator")
|
||||||
|
val gift = saveGift(
|
||||||
|
applicationNo = "G-SENT-DETAIL",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.RECEIVED
|
||||||
|
)
|
||||||
|
val delivery = saveDelivery(gift)
|
||||||
|
delivery.recipientName = "수신자"
|
||||||
|
delivery.recipientPhoneNumber = "01098765432"
|
||||||
|
delivery.recipientZipCode = "04524"
|
||||||
|
delivery.recipientAddress = "서울시 중구"
|
||||||
|
delivery.recipientAddressDetail = "202호"
|
||||||
|
|
||||||
|
val result = queryService().getGiftDetail(sender.id!!, gift.applicationNo)
|
||||||
|
|
||||||
|
assertEquals(GiftDirection.SENT, result.direction)
|
||||||
|
assertEquals(recipient.nickname, result.giftInfo.recipientCreatorNickname)
|
||||||
|
assertNull(result.giftInfo.senderNickname)
|
||||||
|
assertEquals("소형", result.giftInfo.sizeName)
|
||||||
|
assertEquals("인형", result.giftInfo.categoryName)
|
||||||
|
assertEquals(gift.applicationNo, result.giftInfo.applicationNo)
|
||||||
|
assertEquals(80, result.giftInfo.paidCan)
|
||||||
|
assertNull(result.giftInfo.tracking)
|
||||||
|
assertNull(result.giftInfo.shippingRequestedAt)
|
||||||
|
assertEquals("발신자", result.senderInfo?.name)
|
||||||
|
assertEquals("01012345678", result.senderInfo?.phoneNumber)
|
||||||
|
assertEquals("(06234) 서울시 강남구, 101동 1001호", result.senderInfo?.address)
|
||||||
|
assertNull(result.recipientAddress)
|
||||||
|
assertNull(result.mailbox)
|
||||||
|
assertEquals(true, result.trackingRequired)
|
||||||
|
assertEquals(false, result.recipientAddressRequired)
|
||||||
|
assertEquals(
|
||||||
|
listOf(
|
||||||
|
GiftStatus.RECEIVED,
|
||||||
|
GiftStatus.TRACKING_REGISTERED,
|
||||||
|
GiftStatus.ARRIVED_AT_MAILBOX,
|
||||||
|
GiftStatus.INSPECTION_COMPLETED,
|
||||||
|
GiftStatus.DELIVERED
|
||||||
|
),
|
||||||
|
result.statusTimeline.map { it.status }
|
||||||
|
)
|
||||||
|
assertNotNull(result.statusTimeline[0].occurredAt)
|
||||||
|
assertNull(result.statusTimeline[1].occurredAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("수신자는 노출된 선물 상세에서 자신의 배송지와 발신자 닉네임만 조회한다")
|
||||||
|
fun shouldReturnReceivedGiftDetailWithoutSenderInformation() {
|
||||||
|
val sender = saveMember("fan")
|
||||||
|
val recipient = saveMember("creator")
|
||||||
|
val gift = saveGift(
|
||||||
|
applicationNo = "G-RECEIVED-DETAIL",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.TRACKING_REGISTERED
|
||||||
|
)
|
||||||
|
saveDelivery(gift).apply {
|
||||||
|
trackingRegisteredAt = NOW.plusHours(1)
|
||||||
|
recipientName = "수신자"
|
||||||
|
recipientPhoneNumber = "01098765432"
|
||||||
|
recipientZipCode = "04524"
|
||||||
|
recipientAddress = "서울시 중구"
|
||||||
|
recipientAddressDetail = "202호"
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = queryService().getGiftDetail(recipient.id!!, gift.applicationNo)
|
||||||
|
|
||||||
|
assertEquals(GiftDirection.RECEIVED, result.direction)
|
||||||
|
assertNull(result.giftInfo.recipientCreatorNickname)
|
||||||
|
assertEquals(sender.nickname, result.giftInfo.senderNickname)
|
||||||
|
assertEquals("소형", result.giftInfo.sizeName)
|
||||||
|
assertEquals("인형", result.giftInfo.categoryName)
|
||||||
|
assertNull(result.giftInfo.applicationNo)
|
||||||
|
assertNull(result.giftInfo.paidCan)
|
||||||
|
assertNull(result.giftInfo.tracking)
|
||||||
|
assertNotNull(result.giftInfo.shippingRequestedAt)
|
||||||
|
assertNull(result.senderInfo)
|
||||||
|
assertEquals("수신자", result.recipientAddress?.name)
|
||||||
|
assertEquals("01098765432", result.recipientAddress?.phoneNumber)
|
||||||
|
assertEquals("(04524) 서울시 중구, 202호", result.recipientAddress?.address)
|
||||||
|
assertNull(result.mailbox)
|
||||||
|
assertEquals(false, result.trackingRequired)
|
||||||
|
assertEquals(false, result.recipientAddressRequired)
|
||||||
|
assertNotNull(result.statusTimeline[0].occurredAt)
|
||||||
|
assertEquals(NOW.plusHours(1), result.statusTimeline[1].occurredAt)
|
||||||
|
assertNull(result.statusTimeline[2].occurredAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("상세 조회는 수신자 노출 조건과 관계없는 회원 및 배송 정보 누락을 거부한다")
|
||||||
|
fun shouldRejectHiddenOrUnrelatedGiftDetailAndMissingDelivery() {
|
||||||
|
val sender = saveMember("fan")
|
||||||
|
val recipient = saveMember("creator")
|
||||||
|
val other = saveMember("other")
|
||||||
|
val hiddenGift = saveGift(
|
||||||
|
applicationNo = "G-HIDDEN-DETAIL",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.RECEIVED
|
||||||
|
)
|
||||||
|
saveDelivery(hiddenGift)
|
||||||
|
val visibleGift = saveGift(
|
||||||
|
applicationNo = "G-VISIBLE-DETAIL",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.TRACKING_REGISTERED
|
||||||
|
)
|
||||||
|
saveDelivery(visibleGift)
|
||||||
|
val missingDeliveryGift = saveGift(
|
||||||
|
applicationNo = "G-MISSING-DELIVERY",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.RECEIVED
|
||||||
|
)
|
||||||
|
val service = queryService()
|
||||||
|
|
||||||
|
val hiddenException = assertThrows(SodaException::class.java) {
|
||||||
|
service.getGiftDetail(recipient.id!!, hiddenGift.applicationNo)
|
||||||
|
}
|
||||||
|
val unrelatedException = assertThrows(SodaException::class.java) {
|
||||||
|
service.getGiftDetail(other.id!!, visibleGift.applicationNo)
|
||||||
|
}
|
||||||
|
val missingDeliveryException = assertThrows(SodaException::class.java) {
|
||||||
|
service.getGiftDetail(sender.id!!, missingDeliveryGift.applicationNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("common.error.invalid_request", hiddenException.messageKey)
|
||||||
|
assertEquals("common.error.invalid_request", unrelatedException.messageKey)
|
||||||
|
assertEquals("common.error.invalid_request", missingDeliveryException.messageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("수신자 배송지 입력 필요 여부는 미입력인 진행 상태에서만 참이다")
|
||||||
|
fun shouldRequireRecipientAddressOnlyForNonTerminalReceivedGift() {
|
||||||
|
val sender = saveMember("fan")
|
||||||
|
val recipient = saveMember("creator")
|
||||||
|
val pendingGift = saveGift(
|
||||||
|
applicationNo = "G-ADDRESS-PENDING",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.TRACKING_REGISTERED
|
||||||
|
)
|
||||||
|
saveDelivery(pendingGift).trackingRegisteredAt = NOW
|
||||||
|
val terminalGift = saveGift(
|
||||||
|
applicationNo = "G-ADDRESS-TERMINAL",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.UNDELIVERABLE
|
||||||
|
)
|
||||||
|
saveDelivery(terminalGift).apply {
|
||||||
|
trackingRegisteredAt = NOW
|
||||||
|
undeliverableAt = NOW.plusHours(1)
|
||||||
|
undeliverableReason = "배송지 미입력 기한 초과"
|
||||||
|
}
|
||||||
|
val service = queryService()
|
||||||
|
|
||||||
|
val pendingResult = service.getGiftDetail(recipient.id!!, pendingGift.applicationNo)
|
||||||
|
val terminalResult = service.getGiftDetail(recipient.id!!, terminalGift.applicationNo)
|
||||||
|
|
||||||
|
assertEquals(true, pendingResult.recipientAddressRequired)
|
||||||
|
assertNull(pendingResult.recipientAddress)
|
||||||
|
assertEquals(false, terminalResult.recipientAddressRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("취소와 전달 불가 상세는 정상 진행 타임라인과 종료 정보를 함께 반환한다")
|
||||||
|
fun shouldReturnTerminalInformationWithNormalProgressTimeline() {
|
||||||
|
val sender = saveMember("fan")
|
||||||
|
val recipient = saveMember("creator")
|
||||||
|
val canceledGift = saveGift(
|
||||||
|
applicationNo = "G-CANCELED-DETAIL",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.CANCELED
|
||||||
|
)
|
||||||
|
saveDelivery(canceledGift).canceledAt = NOW.plusHours(1)
|
||||||
|
val undeliverableGift = saveGift(
|
||||||
|
applicationNo = "G-UNDELIVERABLE-X",
|
||||||
|
senderMemberId = sender.id!!,
|
||||||
|
recipientMemberId = recipient.id!!,
|
||||||
|
status = GiftStatus.UNDELIVERABLE
|
||||||
|
)
|
||||||
|
saveDelivery(undeliverableGift).apply {
|
||||||
|
trackingRegisteredAt = NOW.plusHours(1)
|
||||||
|
arrivedAtMailboxAt = NOW.plusHours(2)
|
||||||
|
undeliverableAt = NOW.plusHours(3)
|
||||||
|
undeliverableReason = "검수 실패"
|
||||||
|
}
|
||||||
|
val service = queryService()
|
||||||
|
|
||||||
|
val canceledResult = service.getGiftDetail(sender.id!!, canceledGift.applicationNo)
|
||||||
|
val undeliverableResult = service.getGiftDetail(sender.id!!, undeliverableGift.applicationNo)
|
||||||
|
|
||||||
|
assertEquals(NOW.plusHours(1), canceledResult.delivery.canceledAt)
|
||||||
|
assertNull(canceledResult.statusTimeline[1].occurredAt)
|
||||||
|
assertEquals(NOW.plusHours(1), undeliverableResult.statusTimeline[1].occurredAt)
|
||||||
|
assertEquals(NOW.plusHours(2), undeliverableResult.statusTimeline[2].occurredAt)
|
||||||
|
assertNull(undeliverableResult.statusTimeline[3].occurredAt)
|
||||||
|
assertEquals(NOW.plusHours(3), undeliverableResult.delivery.undeliverableAt)
|
||||||
|
assertEquals("검수 실패", undeliverableResult.delivery.undeliverableReason)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveGift(
|
||||||
|
applicationNo: String,
|
||||||
|
senderMemberId: Long,
|
||||||
|
recipientMemberId: Long,
|
||||||
|
status: GiftStatus
|
||||||
|
) = giftRepository.save(
|
||||||
|
Gift(
|
||||||
|
applicationNo = applicationNo,
|
||||||
|
senderMemberId = senderMemberId,
|
||||||
|
recipientMemberId = recipientMemberId,
|
||||||
|
status = status,
|
||||||
|
sizeCode = GiftSize.SMALL,
|
||||||
|
categoryId = 1L,
|
||||||
|
categoryNameSnapshot = "인형",
|
||||||
|
salePriceCan = 80,
|
||||||
|
canUsageId = 1L,
|
||||||
|
senderTermsAgreed = true,
|
||||||
|
senderPrivacyAgreed = true,
|
||||||
|
senderTermsAgreedAt = NOW,
|
||||||
|
senderPrivacyAgreedAt = NOW
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun saveMember(nickname: String) = memberRepository.save(Member(password = "password", nickname = nickname))
|
||||||
|
|
||||||
|
private fun saveDelivery(gift: Gift) = deliveryRepository.save(
|
||||||
|
GiftDelivery(
|
||||||
|
giftId = gift.id!!,
|
||||||
|
senderName = "발신자",
|
||||||
|
senderPhoneNumber = "01012345678",
|
||||||
|
senderZipCode = "06234",
|
||||||
|
senderAddress = "서울시 강남구",
|
||||||
|
senderAddressDetail = "101동 1001호",
|
||||||
|
trackingDeadlineAt = NOW.plusDays(3)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun queryService() = GiftQueryService(
|
||||||
|
categoryRepository,
|
||||||
|
sizePriceRepository,
|
||||||
|
giftRepository,
|
||||||
|
deliveryRepository,
|
||||||
|
memberRepository
|
||||||
|
)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val MEMBER_ID = 1L
|
||||||
|
val NOW: LocalDateTime = LocalDateTime.of(2026, 9, 29, 12, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user