feat(gift): 선물 신청 명령 서비스를 추가한다

This commit is contained in:
2026-09-30 13:37:06 +09:00
parent 71e7195414
commit 95b3b16897
2 changed files with 1236 additions and 0 deletions
@@ -0,0 +1,368 @@
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.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.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.GiftReview
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftReviewRepository
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.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
data class GiftApplicationCommand(
val recipientMemberId: Long,
val senderName: String,
val senderPhoneNumber: String,
val senderZipCode: String,
val senderAddress: String,
val senderAddressDetail: String?,
val sizeCode: GiftSize,
val categoryId: Long,
val senderTermsAgreed: Boolean,
val senderPrivacyAgreed: Boolean,
val damageWaiverAgreed: Boolean
)
data class GiftApplicationResult(
val applicationNo: String,
val status: GiftStatus,
val statusName: String,
val priceCan: Int,
val trackingDeadlineAt: LocalDateTime
)
data class GiftCancellationResult(
val applicationNo: String,
val status: GiftStatus,
val statusName: String,
val priceCan: Int,
val canceledAt: LocalDateTime
)
data class GiftTrackingRegistrationCommand(
val courierCompanyName: String,
val trackingNumber: String
)
data class GiftTrackingRegistrationResult(
val applicationNo: String,
val status: GiftStatus,
val statusName: String,
val courierCompanyName: String,
val trackingNumber: String,
val trackingRegisteredAt: LocalDateTime,
val recipientAddressDeadlineAt: LocalDateTime
)
data class GiftRecipientAddressRegistrationCommand(
val name: String,
val phoneNumber: String,
val zipCode: String,
val address: String,
val addressDetail: String?,
val recipientTermsAgreed: Boolean,
val recipientPrivacyAgreed: Boolean
)
data class GiftRecipientAddressRegistrationResult(
val applicationNo: String,
val status: GiftStatus,
val statusName: String,
val recipientAddressRegisteredAt: LocalDateTime
)
data class GiftDeliveryConfirmationResult(
val applicationNo: String,
val status: GiftStatus,
val statusName: String,
val deliveredAt: LocalDateTime
)
data class GiftReviewCommand(
val rating: Int,
val keywords: List<String>,
val comment: String?
)
data class GiftReviewResult(
val reviewId: Long,
val applicationNo: String,
val rating: Int,
val keywords: List<String>,
val comment: String?,
val createdAt: LocalDateTime?
)
@Service
class GiftCommandService(
private val giftRepository: GiftRepository,
private val deliveryRepository: GiftDeliveryRepository,
private val reviewRepository: GiftReviewRepository,
private val categoryRepository: GiftCategoryRepository,
private val sizePriceRepository: GiftSizePriceRepository,
private val memberRepository: MemberRepository,
private val applicationNoGenerator: GiftApplicationNoGenerator,
private val canPaymentService: CanPaymentService,
private val giftPushService: GiftPushService,
private val now: () -> LocalDateTime = { LocalDateTime.now() }
) {
@Transactional
fun registerGift(member: Member, command: GiftApplicationCommand): GiftApplicationResult {
validate(command)
val senderMemberId = member.id ?: throw invalidRequest()
memberRepository.findCreatorByIdOrNull(command.recipientMemberId) ?: throw invalidRequest()
val category = categoryRepository.findByIdOrNull(command.categoryId)?.takeIf { it.isActive } ?: throw invalidRequest()
if (category.requiresDamageWaiver && !command.damageWaiverAgreed) throw invalidRequest()
val sizePrice = sizePriceRepository.findBySizeCode(command.sizeCode)?.takeIf { it.isActive } ?: throw invalidRequest()
val appliedAt = now()
val trackingDeadlineAt = appliedAt.plusDays(3)
val applicationNo = applicationNoGenerator.generate(appliedAt)
val canUsageId = canPaymentService.spendGiftCan(
senderMemberId,
sizePrice.salePriceCan,
member.container.ifBlank { "web" }
)
val gift = giftRepository.save(
Gift(
applicationNo = applicationNo,
senderMemberId = senderMemberId,
recipientMemberId = command.recipientMemberId,
status = GiftStatus.RECEIVED,
sizeCode = command.sizeCode,
categoryId = category.id ?: throw invalidRequest(),
categoryNameSnapshot = category.name,
salePriceCan = sizePrice.salePriceCan,
canUsageId = canUsageId,
senderTermsAgreed = command.senderTermsAgreed,
senderPrivacyAgreed = command.senderPrivacyAgreed,
senderTermsAgreedAt = appliedAt,
senderPrivacyAgreedAt = appliedAt,
damageWaiverAgreed = command.damageWaiverAgreed
)
)
deliveryRepository.save(
GiftDelivery(
giftId = gift.id ?: throw invalidRequest(),
senderName = command.senderName,
senderPhoneNumber = command.senderPhoneNumber,
senderZipCode = command.senderZipCode,
senderAddress = command.senderAddress,
senderAddressDetail = command.senderAddressDetail,
trackingDeadlineAt = trackingDeadlineAt
)
)
return GiftApplicationResult(
applicationNo = applicationNo,
status = GiftStatus.RECEIVED,
statusName = GiftStatus.RECEIVED.displayName,
priceCan = sizePrice.salePriceCan,
trackingDeadlineAt = trackingDeadlineAt
)
}
@Transactional
fun writeReview(member: Member, applicationNo: String, command: GiftReviewCommand): GiftReviewResult {
val senderMemberId = member.id ?: throw invalidRequest()
val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest()
val giftId = gift.id ?: throw invalidRequest()
if (
gift.senderMemberId != senderMemberId ||
gift.status != GiftStatus.DELIVERED ||
reviewRepository.findByGiftId(giftId) != null
) {
throw invalidRequest()
}
validate(command)
val review = reviewRepository.save(
GiftReview(
giftId = giftId,
senderMemberId = senderMemberId,
rating = command.rating,
keywords = command.keywords.joinToString(","),
comment = command.comment
)
)
return GiftReviewResult(
reviewId = review.id ?: throw invalidRequest(),
applicationNo = gift.applicationNo,
rating = review.rating,
keywords = review.keywords?.takeIf { it.isNotBlank() }?.split(",") ?: emptyList(),
comment = review.comment,
createdAt = review.createdAt
)
}
@Transactional
fun cancelGift(member: Member, applicationNo: String): GiftCancellationResult {
val senderMemberId = member.id ?: throw invalidRequest()
val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest()
if (gift.senderMemberId != senderMemberId || gift.status != GiftStatus.RECEIVED) throw invalidRequest()
val canceledAt = now()
gift.status = GiftStatus.CANCELED
deliveryRepository.findByGiftId(gift.id ?: throw invalidRequest())?.canceledAt = canceledAt
canPaymentService.refundGiftCan(senderMemberId, gift.canUsageId)
return GiftCancellationResult(
applicationNo = gift.applicationNo,
status = gift.status,
statusName = gift.status.displayName,
priceCan = gift.salePriceCan,
canceledAt = canceledAt
)
}
@Transactional
fun registerTracking(
member: Member,
applicationNo: String,
command: GiftTrackingRegistrationCommand
): GiftTrackingRegistrationResult {
val senderMemberId = member.id ?: throw invalidRequest()
val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest()
if (gift.senderMemberId != senderMemberId || gift.status != GiftStatus.RECEIVED) throw invalidRequest()
validate(command)
val delivery = deliveryRepository.findByGiftId(gift.id ?: throw invalidRequest()) ?: throw invalidRequest()
val trackingRegisteredAt = now()
val recipientAddressDeadlineAt = trackingRegisteredAt.plusDays(7)
gift.status = GiftStatus.TRACKING_REGISTERED
delivery.courierCompanyName = command.courierCompanyName
delivery.trackingNumber = command.trackingNumber
delivery.trackingRegisteredAt = trackingRegisteredAt
delivery.recipientAddressDeadlineAt = recipientAddressDeadlineAt
giftPushService.sendTrackingRegisteredToRecipient(gift.applicationNo, gift.recipientMemberId)
return GiftTrackingRegistrationResult(
applicationNo = gift.applicationNo,
status = gift.status,
statusName = gift.status.displayName,
courierCompanyName = command.courierCompanyName,
trackingNumber = command.trackingNumber,
trackingRegisteredAt = trackingRegisteredAt,
recipientAddressDeadlineAt = recipientAddressDeadlineAt
)
}
@Transactional
fun registerRecipientAddress(
member: Member,
applicationNo: String,
command: GiftRecipientAddressRegistrationCommand
): GiftRecipientAddressRegistrationResult {
val recipientMemberId = member.id ?: throw invalidRequest()
val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest()
if (gift.recipientMemberId != recipientMemberId || gift.status !in RECIPIENT_ADDRESS_REGISTRABLE_STATUSES) {
throw invalidRequest()
}
validate(command)
val delivery = deliveryRepository.findByGiftId(gift.id ?: throw invalidRequest()) ?: throw invalidRequest()
val registeredAt = now()
delivery.recipientName = command.name
delivery.recipientPhoneNumber = command.phoneNumber
delivery.recipientZipCode = command.zipCode
delivery.recipientAddress = command.address
delivery.recipientAddressDetail = command.addressDetail
gift.recipientTermsAgreed = command.recipientTermsAgreed
gift.recipientPrivacyAgreed = command.recipientPrivacyAgreed
gift.recipientTermsAgreedAt = registeredAt
gift.recipientPrivacyAgreedAt = registeredAt
return GiftRecipientAddressRegistrationResult(
applicationNo = gift.applicationNo,
status = gift.status,
statusName = gift.status.displayName,
recipientAddressRegisteredAt = registeredAt
)
}
@Transactional
fun confirmDelivery(member: Member, applicationNo: String): GiftDeliveryConfirmationResult {
val recipientMemberId = member.id ?: throw invalidRequest()
val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest()
if (gift.recipientMemberId != recipientMemberId || gift.status != GiftStatus.INSPECTION_COMPLETED) {
throw invalidRequest()
}
val delivery = deliveryRepository.findByGiftId(gift.id ?: throw invalidRequest()) ?: throw invalidRequest()
val deliveredAt = now()
gift.status = GiftStatus.DELIVERED
delivery.deliveredAt = deliveredAt
return GiftDeliveryConfirmationResult(
applicationNo = gift.applicationNo,
status = gift.status,
statusName = gift.status.displayName,
deliveredAt = deliveredAt
)
}
private fun validate(command: GiftApplicationCommand) {
if (
command.senderName.isBlank() ||
command.senderPhoneNumber.isBlank() ||
command.senderZipCode.isBlank() ||
command.senderAddress.isBlank() ||
!command.senderTermsAgreed ||
!command.senderPrivacyAgreed
) {
throw invalidRequest()
}
}
private fun validate(command: GiftTrackingRegistrationCommand) {
if (
command.courierCompanyName.isBlank() ||
command.courierCompanyName.length > 50 ||
command.trackingNumber.isBlank()
) {
throw invalidRequest()
}
}
private fun validate(command: GiftRecipientAddressRegistrationCommand) {
if (
command.name.isBlank() ||
command.phoneNumber.isBlank() ||
command.zipCode.isBlank() ||
command.address.isBlank() ||
!command.recipientTermsAgreed ||
!command.recipientPrivacyAgreed
) {
throw invalidRequest()
}
}
private fun validate(command: GiftReviewCommand) {
if (
command.rating !in 1..5 ||
command.keywords.any { it.length > 255 } ||
(command.comment?.length ?: 0) > 255
) {
throw invalidRequest()
}
}
private fun invalidRequest() = SodaException(messageKey = "common.error.invalid_request")
private companion object {
val RECIPIENT_ADDRESS_REGISTRABLE_STATUSES = setOf(
GiftStatus.TRACKING_REGISTERED,
GiftStatus.ARRIVED_AT_MAILBOX,
GiftStatus.INSPECTION_COMPLETED
)
}
}
@@ -0,0 +1,868 @@
package kr.co.vividnext.sodalive.v2.gift
import kr.co.vividnext.sodalive.can.CanRepository
import kr.co.vividnext.sodalive.can.charge.Charge
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
import kr.co.vividnext.sodalive.can.payment.CanPaymentService
import kr.co.vividnext.sodalive.can.payment.Payment
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.UseCan
import kr.co.vividnext.sodalive.can.use.UseCanCalculate
import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository
import kr.co.vividnext.sodalive.can.use.UseCanRepository
import kr.co.vividnext.sodalive.common.CountryContext
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
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.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.application.GiftApplicationCommand
import kr.co.vividnext.sodalive.v2.gift.application.GiftApplicationNoGenerator
import kr.co.vividnext.sodalive.v2.gift.application.GiftCancellationResult
import kr.co.vividnext.sodalive.v2.gift.application.GiftCommandService
import kr.co.vividnext.sodalive.v2.gift.application.GiftDeliveryConfirmationResult
import kr.co.vividnext.sodalive.v2.gift.application.GiftPushService
import kr.co.vividnext.sodalive.v2.gift.application.GiftRecipientAddressRegistrationCommand
import kr.co.vividnext.sodalive.v2.gift.application.GiftRecipientAddressRegistrationResult
import kr.co.vividnext.sodalive.v2.gift.application.GiftReviewCommand
import kr.co.vividnext.sodalive.v2.gift.application.GiftReviewResult
import kr.co.vividnext.sodalive.v2.gift.application.GiftTrackingRegistrationCommand
import kr.co.vividnext.sodalive.v2.gift.application.GiftTrackingRegistrationResult
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.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.MethodSource
import org.mockito.Mockito
import java.time.LocalDateTime
import java.util.Optional
class GiftCommandServiceTest {
@Test
@DisplayName("선물 캔 사용은 한 번만 환불할 수 있다")
fun shouldSpendAndRefundGiftCanOnlyOnce() {
// Given: 충전 캔을 보유한 선물 발신자와 결제 서비스를 준비한다.
val member = Member(password = "password", nickname = "sender")
member.id = MEMBER_ID
member.pgChargeCan = INITIAL_CAN
val charge = Charge(chargeCan = INITIAL_CAN, rewardCan = 0)
charge.id = CHARGE_ID
charge.member = member
charge.payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG)
val fixture = CanPaymentFixture(member, charge)
// When: 선물용 캔을 사용한 뒤 해당 사용 내역을 환불한다.
val useCanId = fixture.service.spendGiftCan(
memberId = MEMBER_ID,
needCan = GIFT_PRICE_CAN,
container = "web"
)
val afterSpendCan = member.getChargeCan("web") + member.getRewardCan("web")
fixture.service.refundGiftCan(memberId = MEMBER_ID, useCanId = useCanId)
val afterRefundCan = member.getChargeCan("web") + member.getRewardCan("web")
// Then: 발신자와 GIFT 용도가 저장되고, 잔액이 복원되며 재환불은 거부된다.
assertEquals(CanUsage.GIFT, fixture.savedUseCan.canUsage)
assertEquals(member, fixture.savedUseCan.member)
assertEquals(INITIAL_CAN - GIFT_PRICE_CAN, afterSpendCan)
assertEquals(INITIAL_CAN, afterRefundCan)
assertEquals(true, fixture.savedUseCan.isRefund)
assertThrows(SodaException::class.java) {
fixture.service.refundGiftCan(memberId = MEMBER_ID, useCanId = useCanId)
}
}
@Test
@DisplayName("선물 신청은 가격과 약관 동의, 배송 기한을 스냅샷으로 저장하고 캔을 차감한다")
fun shouldRegisterGiftWithPriceAgreementAndDeliverySnapshots() {
// Given: 활성 선물 카테고리와 가격, 발신자 및 수신 크리에이터를 준비한다.
val fixture = GiftApplicationFixture()
// When: 팬이 선물을 신청한다.
val result = fixture.service.registerGift(fixture.sender, validGiftApplicationCommand())
// Then: 신청 정보와 배송 기한을 저장하고 판매가만큼 캔을 차감한다.
assertEquals(APPLICATION_NO, result.applicationNo)
assertEquals(GiftStatus.RECEIVED, result.status)
assertEquals(GiftStatus.RECEIVED.displayName, result.statusName)
assertEquals(SALE_PRICE_CAN, result.priceCan)
assertEquals(NOW.plusDays(3), result.trackingDeadlineAt)
assertEquals(SENDER_ID, fixture.savedGift.senderMemberId)
assertEquals(RECIPIENT_ID, fixture.savedGift.recipientMemberId)
assertEquals(GiftStatus.RECEIVED, fixture.savedGift.status)
assertEquals(GiftSize.SMALL, fixture.savedGift.sizeCode)
assertEquals(CATEGORY_ID, fixture.savedGift.categoryId)
assertEquals("인형", fixture.savedGift.categoryNameSnapshot)
assertEquals(SALE_PRICE_CAN, fixture.savedGift.salePriceCan)
assertEquals(USE_CAN_ID, fixture.savedGift.canUsageId)
assertEquals(true, fixture.savedGift.senderTermsAgreed)
assertEquals(true, fixture.savedGift.senderPrivacyAgreed)
assertEquals(NOW, fixture.savedGift.senderTermsAgreedAt)
assertEquals(NOW, fixture.savedGift.senderPrivacyAgreedAt)
assertEquals(true, fixture.savedGift.damageWaiverAgreed)
assertEquals(GIFT_ID, fixture.savedDelivery.giftId)
assertEquals("홍길동", fixture.savedDelivery.senderName)
assertEquals("01012345678", fixture.savedDelivery.senderPhoneNumber)
assertEquals("06234", fixture.savedDelivery.senderZipCode)
assertEquals("서울시 강남구", fixture.savedDelivery.senderAddress)
assertEquals("101동 1001호", fixture.savedDelivery.senderAddressDetail)
assertEquals(NOW.plusDays(3), fixture.savedDelivery.trackingDeadlineAt)
Mockito.verify(fixture.canPaymentService).spendGiftCan(SENDER_ID, SALE_PRICE_CAN, "aos")
}
@Test
@DisplayName("보낸 팬은 운송장 등록 전 선물을 취소하고 전액 환불받는다")
fun shouldCancelReceivedGiftAndRefundOnce() {
// Given: 접수 상태의 선물과 발신자 배송 정보를 준비한다.
val fixture = GiftApplicationFixture()
fixture.prepareGiftForCancellation()
// When: 보낸 팬이 선물을 취소한다.
val result = fixture.service.cancelGift(fixture.sender, APPLICATION_NO)
// Then: 선물과 배송 정보에 취소 시각을 저장하고 한 번만 환불한다.
assertEquals(
GiftCancellationResult(
applicationNo = APPLICATION_NO,
status = GiftStatus.CANCELED,
statusName = GiftStatus.CANCELED.displayName,
priceCan = SALE_PRICE_CAN,
canceledAt = NOW
),
result
)
assertEquals(GiftStatus.CANCELED, fixture.cancellationGift.status)
assertEquals(NOW, fixture.cancellationDelivery.canceledAt)
Mockito.verify(fixture.canPaymentService).refundGiftCan(SENDER_ID, USE_CAN_ID)
}
@Test
@DisplayName("보낸 팬은 접수된 선물의 운송장을 등록하고 크리에이터 배송지 입력 기한을 생성한다")
fun shouldRegisterTrackingForReceivedGift() {
// Given: 접수 상태의 선물과 발신자 배송 정보를 준비한다.
val fixture = GiftApplicationFixture()
fixture.prepareGiftForTracking()
// When: 보낸 팬이 택배사와 운송장 번호를 등록한다.
val result = fixture.service.registerTracking(
fixture.sender,
APPLICATION_NO,
validGiftTrackingRegistrationCommand()
)
// Then: 발송 확인 상태와 운송장 정보, 수신자 주소 입력 기한을 저장한다.
assertEquals(
GiftTrackingRegistrationResult(
applicationNo = APPLICATION_NO,
status = GiftStatus.TRACKING_REGISTERED,
statusName = GiftStatus.TRACKING_REGISTERED.displayName,
courierCompanyName = "CJ대한통운",
trackingNumber = "1234567890",
trackingRegisteredAt = NOW,
recipientAddressDeadlineAt = NOW.plusDays(7)
),
result
)
assertEquals(GiftStatus.TRACKING_REGISTERED, fixture.trackingGift.status)
assertEquals("CJ대한통운", fixture.trackingDelivery.courierCompanyName)
assertEquals("1234567890", fixture.trackingDelivery.trackingNumber)
assertEquals(NOW, fixture.trackingDelivery.trackingRegisteredAt)
assertEquals(NOW.plusDays(7), fixture.trackingDelivery.recipientAddressDeadlineAt)
Mockito.verify(fixture.giftPushService).sendTrackingRegisteredToRecipient(APPLICATION_NO, RECIPIENT_ID)
}
@ParameterizedTest
@MethodSource("recipientAddressRegistrableStatuses")
@DisplayName("받는 크리에이터는 운송장 등록 이후 종료 전 상태에서 주소와 수령 동의를 등록한다")
fun shouldRegisterRecipientAddressAndAgreements(status: GiftStatus) {
// Given: 주소 입력이 허용된 선물과 받는 크리에이터를 준비한다.
val fixture = GiftApplicationFixture()
fixture.prepareGiftForRecipientAddress(status = status)
// When: 받는 크리에이터가 배송지와 수령 약관 동의를 등록한다.
val result = fixture.service.registerRecipientAddress(
fixture.creator,
APPLICATION_NO,
validGiftRecipientAddressRegistrationCommand()
)
// Then: 주소 스냅샷과 두 동의 시각을 저장하고 현재 상태를 반환한다.
assertEquals(
GiftRecipientAddressRegistrationResult(
applicationNo = APPLICATION_NO,
status = status,
statusName = status.displayName,
recipientAddressRegisteredAt = NOW
),
result
)
assertEquals("김소다", fixture.recipientAddressDelivery.recipientName)
assertEquals("01098765432", fixture.recipientAddressDelivery.recipientPhoneNumber)
assertEquals("04524", fixture.recipientAddressDelivery.recipientZipCode)
assertEquals("서울시 중구", fixture.recipientAddressDelivery.recipientAddress)
assertEquals("202호", fixture.recipientAddressDelivery.recipientAddressDetail)
assertEquals(true, fixture.recipientAddressGift.recipientTermsAgreed)
assertEquals(true, fixture.recipientAddressGift.recipientPrivacyAgreed)
assertEquals(NOW, fixture.recipientAddressGift.recipientTermsAgreedAt)
assertEquals(NOW, fixture.recipientAddressGift.recipientPrivacyAgreedAt)
}
@Test
@DisplayName("받는 크리에이터는 검수 완료 선물의 수령을 확인하고 전달 완료 시각을 저장한다")
fun shouldConfirmDeliveryForInspectionCompletedGift() {
val fixture = GiftApplicationFixture()
fixture.prepareGiftForDeliveryConfirmation()
val result = fixture.service.confirmDelivery(fixture.creator, APPLICATION_NO)
assertEquals(
GiftDeliveryConfirmationResult(
applicationNo = APPLICATION_NO,
status = GiftStatus.DELIVERED,
statusName = GiftStatus.DELIVERED.displayName,
deliveredAt = NOW
),
result
)
assertEquals(GiftStatus.DELIVERED, fixture.deliveryConfirmationGift.status)
assertEquals(NOW, fixture.deliveryConfirmationDelivery.deliveredAt)
Mockito.verifyNoInteractions(fixture.giftPushService)
}
@Test
@DisplayName("보낸 팬은 전달 완료 선물에 리뷰를 한 번 작성한다")
fun shouldWriteReviewForDeliveredGiftBySender() {
val fixture = GiftApplicationFixture()
fixture.prepareGiftForReview()
val result = fixture.service.writeReview(fixture.sender, APPLICATION_NO, validGiftReviewCommand())
assertEquals(
GiftReviewResult(
reviewId = REVIEW_ID,
applicationNo = APPLICATION_NO,
rating = 5,
keywords = listOf("배송이 빨라요", "안내가 친절해요"),
comment = "선물 전달 과정이 만족스러웠습니다.",
createdAt = NOW
),
result
)
assertEquals(GIFT_ID, fixture.savedReview.giftId)
assertEquals(SENDER_ID, fixture.savedReview.senderMemberId)
assertEquals(5, fixture.savedReview.rating)
assertEquals("배송이 빨라요,안내가 친절해요", fixture.savedReview.keywords)
assertEquals("선물 전달 과정이 만족스러웠습니다.", fixture.savedReview.comment)
}
@ParameterizedTest
@MethodSource("invalidGiftReviewScenarios")
@DisplayName("리뷰 작성은 보낸 팬의 전달 완료 선물, 1회, 유효한 별점과 내용만 허용한다")
fun shouldRejectInvalidGiftReview(scenario: InvalidGiftReviewScenario) {
val fixture = GiftApplicationFixture()
fixture.prepareGiftForReview(
status = scenario.status,
senderMemberId = scenario.senderMemberId,
reviewExists = scenario.reviewExists
)
val requester = Member(password = "password", nickname = "requester").apply { id = scenario.requesterMemberId }
val exception = assertThrows(SodaException::class.java) {
fixture.service.writeReview(requester, APPLICATION_NO, scenario.command)
}
assertEquals("common.error.invalid_request", exception.messageKey)
Mockito.verify(fixture.reviewRepository, Mockito.never()).save(Mockito.any(GiftReview::class.java))
}
@ParameterizedTest
@MethodSource("invalidGiftDeliveryConfirmationScenarios")
@DisplayName("수령 확인은 받는 크리에이터의 검수 완료 선물에만 가능하고 배송 정보가 필요하다")
fun shouldRejectInvalidGiftDeliveryConfirmation(scenario: InvalidGiftDeliveryConfirmationScenario) {
val fixture = GiftApplicationFixture()
fixture.prepareGiftForDeliveryConfirmation(
status = scenario.status,
recipientMemberId = scenario.recipientMemberId,
deliveryExists = scenario.deliveryExists
)
val requester = Member(password = "password", nickname = "requester").apply { id = scenario.requesterMemberId }
val exception = assertThrows(SodaException::class.java) {
fixture.service.confirmDelivery(requester, APPLICATION_NO)
}
assertEquals("common.error.invalid_request", exception.messageKey)
assertEquals(scenario.status, fixture.deliveryConfirmationGift.status)
assertEquals(null, fixture.deliveryConfirmationDelivery.deliveredAt)
}
@ParameterizedTest
@MethodSource("invalidGiftRecipientAddressRegistrationScenarios")
@DisplayName("수령자 주소 등록은 받는 크리에이터, 허용 상태, 필수 주소와 두 동의가 필요하다")
fun shouldRejectInvalidGiftRecipientAddressRegistration(scenario: InvalidGiftRecipientAddressRegistrationScenario) {
// Given: 권한, 상태 또는 입력값이 유효하지 않은 선물을 준비한다.
val fixture = GiftApplicationFixture()
fixture.prepareGiftForRecipientAddress(
status = scenario.status,
recipientMemberId = scenario.recipientMemberId,
deliveryExists = scenario.deliveryExists
)
val requester = Member(password = "password", nickname = "requester").apply { id = scenario.requesterMemberId }
// When: 배송지 등록을 요청한다.
val exception = assertThrows(SodaException::class.java) {
fixture.service.registerRecipientAddress(requester, APPLICATION_NO, scenario.command)
}
// Then: 공통 요청 오류를 반환하고 선물 상태와 수령 동의를 바꾸지 않는다.
assertEquals("common.error.invalid_request", exception.messageKey)
assertEquals(scenario.status, fixture.recipientAddressGift.status)
assertEquals(false, fixture.recipientAddressGift.recipientTermsAgreed)
assertEquals(false, fixture.recipientAddressGift.recipientPrivacyAgreed)
}
@ParameterizedTest
@MethodSource("invalidGiftTrackingRegistrationScenarios")
@DisplayName("운송장 등록은 보낸 팬의 접수 상태에서 유효한 택배사와 운송장 번호가 있어야 한다")
fun shouldRejectInvalidGiftTrackingRegistration(scenario: InvalidGiftTrackingRegistrationScenario) {
// Given: 운송장 등록 권한, 상태, 입력값 또는 배송 정보가 유효하지 않은 선물을 준비한다.
val fixture = GiftApplicationFixture()
fixture.prepareGiftForTracking(scenario.status, scenario.senderMemberId, scenario.deliveryExists)
val requester = Member(password = "password", nickname = "requester").apply { id = scenario.requesterMemberId }
// When: 운송장 등록을 요청한다.
val exception = assertThrows(SodaException::class.java) {
fixture.service.registerTracking(requester, APPLICATION_NO, scenario.command)
}
// Then: 공통 요청 오류를 반환하고 선물 상태를 바꾸지 않는다.
assertEquals("common.error.invalid_request", exception.messageKey)
assertEquals(scenario.status, fixture.trackingGift.status)
}
@ParameterizedTest
@MethodSource("invalidGiftCancellationScenarios")
@DisplayName("선물 취소는 보낸 팬의 접수 상태에서만 가능하다")
fun shouldRejectGiftCancellationForInvalidSenderOrStatus(scenario: InvalidGiftCancellationScenario) {
// Given: 취소 권한 또는 상태가 유효하지 않은 선물을 준비한다.
val fixture = GiftApplicationFixture()
fixture.prepareGiftForCancellation(scenario.status, scenario.senderMemberId)
val requester = Member(password = "password", nickname = "requester").apply { id = scenario.requesterMemberId }
// When: 취소를 요청한다.
val exception = assertThrows(SodaException::class.java) {
fixture.service.cancelGift(requester, APPLICATION_NO)
}
// Then: 환불하지 않고 공통 요청 오류를 반환한다.
assertEquals("common.error.invalid_request", exception.messageKey)
Mockito.verify(fixture.canPaymentService, Mockito.never()).refundGiftCan(
Mockito.anyLong(),
Mockito.anyLong()
)
}
@ParameterizedTest
@MethodSource("invalidGiftApplicationScenarios")
@DisplayName("선물 신청은 필수 발신자 정보, 동의, 활성 옵션 및 수신 크리에이터를 검증한다")
fun shouldRejectInvalidGiftApplication(scenario: InvalidGiftApplicationScenario) {
// Given: 하나의 신청 조건만 유효하지 않은 선물 신청을 준비한다.
val fixture = GiftApplicationFixture()
fixture.category.isActive = scenario.categoryActive
fixture.category.requiresDamageWaiver = scenario.requiresDamageWaiver
fixture.sizePrice.isActive = scenario.sizePriceActive
fixture.setCreatorExists(scenario.creatorExists)
// When: 유효하지 않은 신청을 등록한다.
val exception = assertThrows(SodaException::class.java) {
fixture.service.registerGift(fixture.sender, scenario.command)
}
// Then: 캔을 차감하지 않고 공통 요청 오류를 반환한다.
assertEquals("common.error.invalid_request", exception.messageKey)
Mockito.verifyNoInteractions(fixture.canPaymentService)
}
private class CanPaymentFixture(member: Member, charge: Charge) {
private val canRepository = Mockito.mock(CanRepository::class.java)
private val memberRepository = Mockito.mock(MemberRepository::class.java)
private val chargeRepository = Mockito.mock(ChargeRepository::class.java)
private val useCanRepository = Mockito.mock(UseCanRepository::class.java)
private val useCanCalculateRepository = Mockito.mock(UseCanCalculateRepository::class.java)
private val useCanCalculates = mutableListOf<UseCanCalculate>()
lateinit var savedUseCan: UseCan
private set
val service = CanPaymentService(
repository = canRepository,
memberRepository = memberRepository,
chargeRepository = chargeRepository,
useCanRepository = useCanRepository,
useCanCalculateRepository = useCanCalculateRepository,
messageSource = SodaMessageSource(),
langContext = LangContext(),
countryContext = CountryContext()
)
init {
Mockito.`when`(memberRepository.findById(MEMBER_ID)).thenReturn(Optional.of(member))
Mockito.`when`(
chargeRepository.getOldestChargeWhereRewardCanGreaterThan0(0L, MEMBER_ID, "web")
).thenReturn(null)
Mockito.`when`(
chargeRepository.getOldestChargeWhereChargeCanGreaterThan0(0L, MEMBER_ID, "web")
).thenReturn(charge)
Mockito.`when`(useCanRepository.save(Mockito.any(UseCan::class.java))).thenAnswer { invocation ->
invocation.getArgument<UseCan>(0).apply {
id = USE_CAN_ID
savedUseCan = this
}
}
Mockito.`when`(useCanRepository.findById(USE_CAN_ID)).thenAnswer { Optional.of(savedUseCan) }
Mockito.`when`(useCanCalculateRepository.save(Mockito.any(UseCanCalculate::class.java))).thenAnswer {
invocation ->
invocation.getArgument<UseCanCalculate>(0).also(useCanCalculates::add)
}
Mockito.`when`(useCanCalculateRepository.findByUseCanIdAndStatus(USE_CAN_ID)).thenReturn(useCanCalculates)
}
}
private class GiftApplicationFixture {
val giftRepository = Mockito.mock(GiftRepository::class.java)
val deliveryRepository = Mockito.mock(GiftDeliveryRepository::class.java)
val reviewRepository = Mockito.mock(GiftReviewRepository::class.java)
val categoryRepository = Mockito.mock(GiftCategoryRepository::class.java)
val sizePriceRepository = Mockito.mock(GiftSizePriceRepository::class.java)
val memberRepository = Mockito.mock(MemberRepository::class.java)
val applicationNoGenerator = Mockito.mock(GiftApplicationNoGenerator::class.java)
val canPaymentService = Mockito.mock(CanPaymentService::class.java)
val giftPushService = Mockito.mock(GiftPushService::class.java)
val sender = Member(password = "password", nickname = "sender", container = "aos").apply { id = SENDER_ID }
val creator = Member(password = "password", nickname = "creator").apply { id = RECIPIENT_ID }
val category = GiftCategory("DOLL", "인형", false, true).apply { id = CATEGORY_ID }
val sizePrice = GiftSizePrice(GiftSize.SMALL, BASE_PRICE_CAN, SALE_PRICE_CAN, true)
lateinit var savedGift: Gift
lateinit var savedDelivery: GiftDelivery
lateinit var cancellationGift: Gift
lateinit var cancellationDelivery: GiftDelivery
lateinit var trackingGift: Gift
lateinit var trackingDelivery: GiftDelivery
lateinit var recipientAddressGift: Gift
lateinit var recipientAddressDelivery: GiftDelivery
lateinit var deliveryConfirmationGift: Gift
lateinit var deliveryConfirmationDelivery: GiftDelivery
lateinit var reviewGift: Gift
lateinit var savedReview: GiftReview
val service = GiftCommandService(
giftRepository = giftRepository,
deliveryRepository = deliveryRepository,
reviewRepository = reviewRepository,
categoryRepository = categoryRepository,
sizePriceRepository = sizePriceRepository,
memberRepository = memberRepository,
applicationNoGenerator = applicationNoGenerator,
canPaymentService = canPaymentService,
giftPushService = giftPushService,
now = { NOW }
)
init {
setCreatorExists(true)
Mockito.`when`(categoryRepository.findById(CATEGORY_ID)).thenReturn(Optional.of(category))
Mockito.`when`(sizePriceRepository.findBySizeCode(GiftSize.SMALL)).thenReturn(sizePrice)
Mockito.`when`(applicationNoGenerator.generate(NOW)).thenReturn(APPLICATION_NO)
Mockito.`when`(canPaymentService.spendGiftCan(SENDER_ID, SALE_PRICE_CAN, "aos")).thenReturn(USE_CAN_ID)
Mockito.`when`(giftRepository.save(Mockito.any(Gift::class.java))).thenAnswer { invocation ->
invocation.getArgument<Gift>(0).apply {
id = GIFT_ID
savedGift = this
}
}
Mockito.`when`(deliveryRepository.save(Mockito.any(GiftDelivery::class.java))).thenAnswer { invocation ->
invocation.getArgument<GiftDelivery>(0).also { savedDelivery = it }
}
Mockito.`when`(reviewRepository.save(Mockito.any(GiftReview::class.java))).thenAnswer { invocation ->
invocation.getArgument<GiftReview>(0).apply {
id = REVIEW_ID
createdAt = NOW
savedReview = this
}
}
}
fun setCreatorExists(exists: Boolean) {
Mockito.`when`(memberRepository.findCreatorByIdOrNull(RECIPIENT_ID)).thenReturn(creator.takeIf { exists })
}
fun prepareGiftForCancellation(
status: GiftStatus = GiftStatus.RECEIVED,
senderMemberId: Long = SENDER_ID
) {
cancellationGift = Gift(
applicationNo = APPLICATION_NO,
senderMemberId = senderMemberId,
recipientMemberId = RECIPIENT_ID,
status = status,
sizeCode = GiftSize.SMALL,
categoryId = CATEGORY_ID,
categoryNameSnapshot = "인형",
salePriceCan = SALE_PRICE_CAN,
canUsageId = USE_CAN_ID,
senderTermsAgreed = true,
senderPrivacyAgreed = true,
senderTermsAgreedAt = NOW,
senderPrivacyAgreedAt = NOW,
damageWaiverAgreed = true
).apply { id = GIFT_ID }
cancellationDelivery = GiftDelivery(
giftId = GIFT_ID,
senderName = "홍길동",
senderPhoneNumber = "01012345678",
senderZipCode = "06234",
senderAddress = "서울시 강남구",
trackingDeadlineAt = NOW.plusDays(3)
)
Mockito.`when`(giftRepository.findByApplicationNo(APPLICATION_NO)).thenReturn(cancellationGift)
Mockito.`when`(deliveryRepository.findByGiftId(GIFT_ID)).thenReturn(cancellationDelivery)
}
fun prepareGiftForTracking(
status: GiftStatus = GiftStatus.RECEIVED,
senderMemberId: Long = SENDER_ID,
deliveryExists: Boolean = true
) {
trackingGift = Gift(
applicationNo = APPLICATION_NO,
senderMemberId = senderMemberId,
recipientMemberId = RECIPIENT_ID,
status = status,
sizeCode = GiftSize.SMALL,
categoryId = CATEGORY_ID,
categoryNameSnapshot = "인형",
salePriceCan = SALE_PRICE_CAN,
canUsageId = USE_CAN_ID,
senderTermsAgreed = true,
senderPrivacyAgreed = true,
senderTermsAgreedAt = NOW,
senderPrivacyAgreedAt = NOW,
damageWaiverAgreed = true
).apply { id = GIFT_ID }
trackingDelivery = GiftDelivery(
giftId = GIFT_ID,
senderName = "홍길동",
senderPhoneNumber = "01012345678",
senderZipCode = "06234",
senderAddress = "서울시 강남구",
trackingDeadlineAt = NOW.plusDays(3)
)
Mockito.`when`(giftRepository.findByApplicationNo(APPLICATION_NO)).thenReturn(trackingGift)
Mockito.`when`(deliveryRepository.findByGiftId(GIFT_ID)).thenReturn(
trackingDelivery.takeIf { deliveryExists }
)
}
fun prepareGiftForRecipientAddress(
status: GiftStatus = GiftStatus.TRACKING_REGISTERED,
recipientMemberId: Long = RECIPIENT_ID,
deliveryExists: Boolean = true
) {
recipientAddressGift = Gift(
applicationNo = APPLICATION_NO,
senderMemberId = SENDER_ID,
recipientMemberId = recipientMemberId,
status = status,
sizeCode = GiftSize.SMALL,
categoryId = CATEGORY_ID,
categoryNameSnapshot = "인형",
salePriceCan = SALE_PRICE_CAN,
canUsageId = USE_CAN_ID,
senderTermsAgreed = true,
senderPrivacyAgreed = true,
senderTermsAgreedAt = NOW,
senderPrivacyAgreedAt = NOW,
damageWaiverAgreed = true
).apply { id = GIFT_ID }
recipientAddressDelivery = GiftDelivery(
giftId = GIFT_ID,
senderName = "홍길동",
senderPhoneNumber = "01012345678",
senderZipCode = "06234",
senderAddress = "서울시 강남구",
trackingDeadlineAt = NOW.plusDays(3)
)
Mockito.`when`(giftRepository.findByApplicationNo(APPLICATION_NO)).thenReturn(recipientAddressGift)
Mockito.`when`(deliveryRepository.findByGiftId(GIFT_ID)).thenReturn(
recipientAddressDelivery.takeIf { deliveryExists }
)
}
fun prepareGiftForDeliveryConfirmation(
status: GiftStatus = GiftStatus.INSPECTION_COMPLETED,
recipientMemberId: Long = RECIPIENT_ID,
deliveryExists: Boolean = true
) {
deliveryConfirmationGift = Gift(
applicationNo = APPLICATION_NO,
senderMemberId = SENDER_ID,
recipientMemberId = recipientMemberId,
status = status,
sizeCode = GiftSize.SMALL,
categoryId = CATEGORY_ID,
categoryNameSnapshot = "인형",
salePriceCan = SALE_PRICE_CAN,
canUsageId = USE_CAN_ID,
senderTermsAgreed = true,
senderPrivacyAgreed = true,
senderTermsAgreedAt = NOW,
senderPrivacyAgreedAt = NOW,
damageWaiverAgreed = true
).apply { id = GIFT_ID }
deliveryConfirmationDelivery = GiftDelivery(
giftId = GIFT_ID,
senderName = "홍길동",
senderPhoneNumber = "01012345678",
senderZipCode = "06234",
senderAddress = "서울시 강남구",
trackingDeadlineAt = NOW.plusDays(3)
)
Mockito.`when`(giftRepository.findByApplicationNo(APPLICATION_NO)).thenReturn(deliveryConfirmationGift)
Mockito.`when`(deliveryRepository.findByGiftId(GIFT_ID)).thenReturn(
deliveryConfirmationDelivery.takeIf { deliveryExists }
)
}
fun prepareGiftForReview(
status: GiftStatus = GiftStatus.DELIVERED,
senderMemberId: Long = SENDER_ID,
reviewExists: Boolean = false
) {
reviewGift = Gift(
applicationNo = APPLICATION_NO,
senderMemberId = senderMemberId,
recipientMemberId = RECIPIENT_ID,
status = status,
sizeCode = GiftSize.SMALL,
categoryId = CATEGORY_ID,
categoryNameSnapshot = "인형",
salePriceCan = SALE_PRICE_CAN,
canUsageId = USE_CAN_ID,
senderTermsAgreed = true,
senderPrivacyAgreed = true,
senderTermsAgreedAt = NOW,
senderPrivacyAgreedAt = NOW,
damageWaiverAgreed = true
).apply { id = GIFT_ID }
Mockito.`when`(giftRepository.findByApplicationNo(APPLICATION_NO)).thenReturn(reviewGift)
Mockito.`when`(reviewRepository.findByGiftId(GIFT_ID)).thenReturn(
GiftReview(GIFT_ID, SENDER_ID, 5).takeIf { reviewExists }
)
}
}
data class InvalidGiftApplicationScenario(
val command: GiftApplicationCommand,
val categoryActive: Boolean = true,
val requiresDamageWaiver: Boolean = false,
val sizePriceActive: Boolean = true,
val creatorExists: Boolean = true
)
data class InvalidGiftCancellationScenario(
val requesterMemberId: Long,
val senderMemberId: Long,
val status: GiftStatus
)
data class InvalidGiftTrackingRegistrationScenario(
val requesterMemberId: Long = SENDER_ID,
val senderMemberId: Long = SENDER_ID,
val status: GiftStatus = GiftStatus.RECEIVED,
val command: GiftTrackingRegistrationCommand = validGiftTrackingRegistrationCommand(),
val deliveryExists: Boolean = true
)
data class InvalidGiftRecipientAddressRegistrationScenario(
val requesterMemberId: Long = RECIPIENT_ID,
val recipientMemberId: Long = RECIPIENT_ID,
val status: GiftStatus = GiftStatus.TRACKING_REGISTERED,
val command: GiftRecipientAddressRegistrationCommand = validGiftRecipientAddressRegistrationCommand(),
val deliveryExists: Boolean = true
)
data class InvalidGiftDeliveryConfirmationScenario(
val requesterMemberId: Long = RECIPIENT_ID,
val recipientMemberId: Long = RECIPIENT_ID,
val status: GiftStatus = GiftStatus.INSPECTION_COMPLETED,
val deliveryExists: Boolean = true
)
data class InvalidGiftReviewScenario(
val requesterMemberId: Long = SENDER_ID,
val senderMemberId: Long = SENDER_ID,
val status: GiftStatus = GiftStatus.DELIVERED,
val command: GiftReviewCommand = validGiftReviewCommand(),
val reviewExists: Boolean = false
)
private companion object {
const val MEMBER_ID = 1L
const val CHARGE_ID = 10L
const val USE_CAN_ID = 100L
const val INITIAL_CAN = 100
const val GIFT_PRICE_CAN = 30
const val SENDER_ID = 2L
const val OTHER_MEMBER_ID = 4L
const val RECIPIENT_ID = 3L
const val CATEGORY_ID = 4L
const val GIFT_ID = 5L
const val REVIEW_ID = 6L
const val APPLICATION_NO = "G20260929000001"
const val BASE_PRICE_CAN = 100
const val SALE_PRICE_CAN = 80
val NOW: LocalDateTime = LocalDateTime.of(2026, 9, 29, 12, 0)
@JvmStatic
fun invalidGiftApplicationScenarios() = listOf(
InvalidGiftApplicationScenario(validGiftApplicationCommand().copy(senderName = "")),
InvalidGiftApplicationScenario(validGiftApplicationCommand().copy(senderPhoneNumber = "")),
InvalidGiftApplicationScenario(validGiftApplicationCommand().copy(senderZipCode = "")),
InvalidGiftApplicationScenario(validGiftApplicationCommand().copy(senderAddress = "")),
InvalidGiftApplicationScenario(validGiftApplicationCommand().copy(senderTermsAgreed = false)),
InvalidGiftApplicationScenario(validGiftApplicationCommand().copy(senderPrivacyAgreed = false)),
InvalidGiftApplicationScenario(validGiftApplicationCommand(), categoryActive = false),
InvalidGiftApplicationScenario(
validGiftApplicationCommand().copy(damageWaiverAgreed = false),
requiresDamageWaiver = true
),
InvalidGiftApplicationScenario(validGiftApplicationCommand(), sizePriceActive = false),
InvalidGiftApplicationScenario(validGiftApplicationCommand(), creatorExists = false)
)
@JvmStatic
fun invalidGiftCancellationScenarios() = listOf(
InvalidGiftCancellationScenario(OTHER_MEMBER_ID, SENDER_ID, GiftStatus.RECEIVED),
InvalidGiftCancellationScenario(SENDER_ID, SENDER_ID, GiftStatus.TRACKING_REGISTERED)
)
@JvmStatic
fun invalidGiftTrackingRegistrationScenarios() = listOf(
InvalidGiftTrackingRegistrationScenario(requesterMemberId = OTHER_MEMBER_ID),
InvalidGiftTrackingRegistrationScenario(status = GiftStatus.TRACKING_REGISTERED),
InvalidGiftTrackingRegistrationScenario(
command = validGiftTrackingRegistrationCommand().copy(courierCompanyName = "")
),
InvalidGiftTrackingRegistrationScenario(
command = validGiftTrackingRegistrationCommand().copy(courierCompanyName = "a".repeat(51))
),
InvalidGiftTrackingRegistrationScenario(
command = validGiftTrackingRegistrationCommand().copy(trackingNumber = "")
),
InvalidGiftTrackingRegistrationScenario(deliveryExists = false)
)
@JvmStatic
fun recipientAddressRegistrableStatuses() = listOf(
GiftStatus.TRACKING_REGISTERED,
GiftStatus.ARRIVED_AT_MAILBOX,
GiftStatus.INSPECTION_COMPLETED
)
@JvmStatic
fun invalidGiftRecipientAddressRegistrationScenarios() = listOf(
InvalidGiftRecipientAddressRegistrationScenario(requesterMemberId = OTHER_MEMBER_ID),
InvalidGiftRecipientAddressRegistrationScenario(status = GiftStatus.RECEIVED),
InvalidGiftRecipientAddressRegistrationScenario(status = GiftStatus.DELIVERED),
InvalidGiftRecipientAddressRegistrationScenario(status = GiftStatus.UNDELIVERABLE),
InvalidGiftRecipientAddressRegistrationScenario(status = GiftStatus.CANCELED),
InvalidGiftRecipientAddressRegistrationScenario(
command = validGiftRecipientAddressRegistrationCommand().copy(name = "")
),
InvalidGiftRecipientAddressRegistrationScenario(
command = validGiftRecipientAddressRegistrationCommand().copy(phoneNumber = "")
),
InvalidGiftRecipientAddressRegistrationScenario(
command = validGiftRecipientAddressRegistrationCommand().copy(zipCode = "")
),
InvalidGiftRecipientAddressRegistrationScenario(
command = validGiftRecipientAddressRegistrationCommand().copy(address = "")
),
InvalidGiftRecipientAddressRegistrationScenario(
command = validGiftRecipientAddressRegistrationCommand().copy(recipientTermsAgreed = false)
),
InvalidGiftRecipientAddressRegistrationScenario(
command = validGiftRecipientAddressRegistrationCommand().copy(recipientPrivacyAgreed = false)
),
InvalidGiftRecipientAddressRegistrationScenario(deliveryExists = false)
)
@JvmStatic
fun invalidGiftDeliveryConfirmationScenarios() = listOf(
InvalidGiftDeliveryConfirmationScenario(requesterMemberId = OTHER_MEMBER_ID),
InvalidGiftDeliveryConfirmationScenario(status = GiftStatus.TRACKING_REGISTERED),
InvalidGiftDeliveryConfirmationScenario(status = GiftStatus.DELIVERED),
InvalidGiftDeliveryConfirmationScenario(deliveryExists = false)
)
@JvmStatic
fun invalidGiftReviewScenarios() = listOf(
InvalidGiftReviewScenario(requesterMemberId = OTHER_MEMBER_ID),
InvalidGiftReviewScenario(status = GiftStatus.INSPECTION_COMPLETED),
InvalidGiftReviewScenario(reviewExists = true),
InvalidGiftReviewScenario(command = validGiftReviewCommand().copy(rating = 0)),
InvalidGiftReviewScenario(command = validGiftReviewCommand().copy(rating = 6)),
InvalidGiftReviewScenario(command = validGiftReviewCommand().copy(keywords = listOf("a".repeat(256)))),
InvalidGiftReviewScenario(command = validGiftReviewCommand().copy(comment = "a".repeat(256)))
)
fun validGiftApplicationCommand() = GiftApplicationCommand(
recipientMemberId = RECIPIENT_ID,
senderName = "홍길동",
senderPhoneNumber = "01012345678",
senderZipCode = "06234",
senderAddress = "서울시 강남구",
senderAddressDetail = "101동 1001호",
sizeCode = GiftSize.SMALL,
categoryId = CATEGORY_ID,
senderTermsAgreed = true,
senderPrivacyAgreed = true,
damageWaiverAgreed = true
)
fun validGiftTrackingRegistrationCommand() = GiftTrackingRegistrationCommand(
courierCompanyName = "CJ대한통운",
trackingNumber = "1234567890"
)
fun validGiftRecipientAddressRegistrationCommand() = GiftRecipientAddressRegistrationCommand(
name = "김소다",
phoneNumber = "01098765432",
zipCode = "04524",
address = "서울시 중구",
addressDetail = "202호",
recipientTermsAgreed = true,
recipientPrivacyAgreed = true
)
fun validGiftReviewCommand() = GiftReviewCommand(
rating = 5,
keywords = listOf("배송이 빨라요", "안내가 친절해요"),
comment = "선물 전달 과정이 만족스러웠습니다."
)
}
}