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,210 @@
package kr.co.vividnext.sodalive.v2.gift.application
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
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.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 GiftCategoryCommand(
val categoryCode: String,
val name: String,
val requiresDamageWaiver: Boolean,
val isActive: Boolean
)
data class GiftCategoryResult(
val categoryId: Long,
val categoryCode: String,
val name: String,
val requiresDamageWaiver: Boolean,
val isActive: Boolean
)
data class GiftSizePriceCommand(
val basePriceCan: Int,
val salePriceCan: Int,
val isActive: Boolean
)
data class GiftSizePriceResult(
val sizeCode: GiftSize,
val sizeName: String,
val basePriceCan: Int,
val salePriceCan: Int,
val isActive: Boolean
)
data class GiftOperationStatusResult(
val applicationNo: String,
val status: GiftStatus,
val statusName: String,
val occurredAt: LocalDateTime
)
@Service
class GiftAdminService(
private val categoryRepository: GiftCategoryRepository,
private val sizePriceRepository: GiftSizePriceRepository,
private val giftRepository: GiftRepository,
private val giftDeliveryRepository: GiftDeliveryRepository,
private val giftPushService: GiftPushService
) {
@Transactional(readOnly = true)
fun getCategories(): List<GiftCategoryResult> = categoryRepository.findAll().map { it.toResult() }
@Transactional
fun createCategory(command: GiftCategoryCommand): GiftCategoryResult {
validateCategory(command)
return categoryRepository.save(
GiftCategory(
categoryCode = command.categoryCode,
name = command.name,
requiresDamageWaiver = command.requiresDamageWaiver,
isActive = command.isActive
)
).toResult()
}
@Transactional
fun updateCategory(categoryId: Long, command: GiftCategoryCommand): GiftCategoryResult {
validateCategory(command)
val category = categoryRepository.findByIdOrNull(categoryId) ?: throw invalidRequest()
category.name = command.name
category.requiresDamageWaiver = command.requiresDamageWaiver
category.isActive = command.isActive
return category.toResult()
}
@Transactional
fun deleteCategory(categoryId: Long): GiftCategoryResult {
val category = categoryRepository.findByIdOrNull(categoryId) ?: throw invalidRequest()
category.isActive = false
return category.toResult()
}
@Transactional(readOnly = true)
fun getSizePrices(): List<GiftSizePriceResult> = sizePriceRepository.findAll().map { it.toResult() }
@Transactional
fun updateSizePrice(sizeCode: GiftSize, command: GiftSizePriceCommand): GiftSizePriceResult {
validateSizePrice(command)
val sizePrice = sizePriceRepository.findBySizeCode(sizeCode)
?: GiftSizePrice(sizeCode = sizeCode, basePriceCan = command.basePriceCan, salePriceCan = command.salePriceCan)
sizePrice.basePriceCan = command.basePriceCan
sizePrice.salePriceCan = command.salePriceCan
sizePrice.isActive = command.isActive
return sizePriceRepository.save(sizePrice).toResult()
}
@Transactional
fun arriveMailbox(applicationNo: String, now: LocalDateTime = LocalDateTime.now()): GiftOperationStatusResult {
val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest()
if (gift.status != GiftStatus.TRACKING_REGISTERED) throw invalidRequest()
val delivery = giftDeliveryRepository.findByGiftId(requireNotNull(gift.id)) ?: throw invalidRequest()
gift.status = GiftStatus.ARRIVED_AT_MAILBOX
delivery.arrivedAtMailboxAt = now
giftPushService.sendArrivedAtMailboxToSender(gift.applicationNo, gift.senderMemberId)
giftPushService.sendArrivedAtMailboxToRecipient(gift.applicationNo, gift.recipientMemberId)
return GiftOperationStatusResult(gift.applicationNo, gift.status, gift.status.displayName, now)
}
@Transactional
fun completeInspection(applicationNo: String, now: LocalDateTime = LocalDateTime.now()): GiftOperationStatusResult {
val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest()
if (gift.status != GiftStatus.ARRIVED_AT_MAILBOX) throw invalidRequest()
val delivery = giftDeliveryRepository.findByGiftId(requireNotNull(gift.id)) ?: throw invalidRequest()
gift.status = GiftStatus.INSPECTION_COMPLETED
delivery.inspectionCompletedAt = now
giftPushService.sendInspectionCompleted(gift.applicationNo, gift.recipientMemberId)
return GiftOperationStatusResult(gift.applicationNo, gift.status, gift.status.displayName, now)
}
@Transactional
fun markUndeliverable(
applicationNo: String,
reason: String,
now: LocalDateTime = LocalDateTime.now()
): GiftOperationStatusResult {
val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest()
if (gift.status !in UNDELIVERABLE_ALLOWED_STATUSES || reason.isBlank() || reason.length > 255) {
throw invalidRequest()
}
val delivery = giftDeliveryRepository.findByGiftId(requireNotNull(gift.id)) ?: throw invalidRequest()
gift.status = GiftStatus.UNDELIVERABLE
delivery.undeliverableAt = now
delivery.undeliverableReason = reason
giftPushService.sendUndeliverableToSender(gift.applicationNo, gift.senderMemberId)
giftPushService.sendUndeliverableToRecipient(gift.applicationNo, gift.recipientMemberId)
return GiftOperationStatusResult(gift.applicationNo, gift.status, gift.status.displayName, now)
}
@Transactional
fun completeDelivery(applicationNo: String, now: LocalDateTime = LocalDateTime.now()): GiftOperationStatusResult {
val gift = giftRepository.findByApplicationNo(applicationNo) ?: throw invalidRequest()
if (gift.status != GiftStatus.INSPECTION_COMPLETED) throw invalidRequest()
val delivery = giftDeliveryRepository.findByGiftId(requireNotNull(gift.id)) ?: throw invalidRequest()
gift.status = GiftStatus.DELIVERED
delivery.deliveredAt = now
giftPushService.sendDeliveredToSender(gift.applicationNo, gift.senderMemberId)
giftPushService.sendDeliveredToRecipient(gift.applicationNo, gift.recipientMemberId)
return GiftOperationStatusResult(gift.applicationNo, gift.status, gift.status.displayName, now)
}
private fun validateCategory(command: GiftCategoryCommand) {
if (
command.categoryCode.isBlank() ||
command.categoryCode.length > 50 ||
command.name.isBlank() ||
command.name.length > 50
) {
throw invalidRequest()
}
}
private fun validateSizePrice(command: GiftSizePriceCommand) {
if (command.salePriceCan <= 0 || command.salePriceCan > command.basePriceCan) throw invalidRequest()
}
private fun GiftCategory.toResult() = GiftCategoryResult(
categoryId = id ?: throw invalidRequest(),
categoryCode = categoryCode,
name = name,
requiresDamageWaiver = requiresDamageWaiver,
isActive = isActive
)
private fun GiftSizePrice.toResult() = GiftSizePriceResult(
sizeCode = sizeCode,
sizeName = sizeCode.displayName,
basePriceCan = basePriceCan,
salePriceCan = salePriceCan,
isActive = isActive
)
private fun invalidRequest() = SodaException(messageKey = "common.error.invalid_request")
private companion object {
val UNDELIVERABLE_ALLOWED_STATUSES = setOf(
GiftStatus.TRACKING_REGISTERED,
GiftStatus.ARRIVED_AT_MAILBOX,
GiftStatus.INSPECTION_COMPLETED
)
}
}