feat(gift): 카테고리별 신청번호를 발급한다
This commit is contained in:
+14
-5
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.gift.application
|
|||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftApplicationNoSequence
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftApplicationNoSequence
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftApplicationNoSequenceRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftApplicationNoSequenceRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategory
|
||||||
import org.springframework.dao.DataIntegrityViolationException
|
import org.springframework.dao.DataIntegrityViolationException
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.PlatformTransactionManager
|
import org.springframework.transaction.PlatformTransactionManager
|
||||||
@@ -18,15 +19,23 @@ class GiftApplicationNoGenerator(
|
|||||||
) {
|
) {
|
||||||
private val transactionTemplate = TransactionTemplate(transactionManager)
|
private val transactionTemplate = TransactionTemplate(transactionManager)
|
||||||
|
|
||||||
fun generate(now: LocalDateTime = LocalDateTime.now(SEOUL_ZONE)): String {
|
fun generate(category: GiftCategory, now: LocalDateTime = LocalDateTime.now(SEOUL_ZONE)): String {
|
||||||
|
val categoryId = category.id ?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
val sequenceDate = now.format(DATE_FORMATTER)
|
val sequenceDate = now.format(DATE_FORMATTER)
|
||||||
for (attempt in 1..MAX_RETRY_COUNT) {
|
for (attempt in 1..MAX_RETRY_COUNT) {
|
||||||
try {
|
try {
|
||||||
return transactionTemplate.execute {
|
return transactionTemplate.execute {
|
||||||
val sequence = repository.findBySequenceDateForUpdate(sequenceDate)
|
val sequence = repository.findByCategoryIdAndSequenceDateForUpdate(categoryId, sequenceDate)
|
||||||
?: repository.saveAndFlush(GiftApplicationNoSequence(sequenceDate = sequenceDate, lastSequence = 0))
|
?: repository.saveAndFlush(
|
||||||
|
GiftApplicationNoSequence(
|
||||||
|
categoryId = categoryId,
|
||||||
|
sequenceDate = sequenceDate,
|
||||||
|
lastSequence = 0
|
||||||
|
)
|
||||||
|
)
|
||||||
sequence.lastSequence += 1
|
sequence.lastSequence += 1
|
||||||
"G$sequenceDate${sequence.lastSequence.toString().padStart(6, '0')}"
|
"${category.receiptCode}-${category.classificationNumber}$sequenceDate" +
|
||||||
|
sequence.lastSequence.toString().padStart(4, '0')
|
||||||
} ?: throw SodaException(messageKey = "common.error.invalid_request")
|
} ?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
} catch (_: DataIntegrityViolationException) {
|
} catch (_: DataIntegrityViolationException) {
|
||||||
if (attempt == MAX_RETRY_COUNT) throw SodaException(messageKey = "gift.application_no.generate_failed")
|
if (attempt == MAX_RETRY_COUNT) throw SodaException(messageKey = "gift.application_no.generate_failed")
|
||||||
@@ -37,7 +46,7 @@ class GiftApplicationNoGenerator(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val SEOUL_ZONE = ZoneId.of("Asia/Seoul")
|
private val SEOUL_ZONE = ZoneId.of("Asia/Seoul")
|
||||||
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd")
|
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("yyMMdd")
|
||||||
private const val MAX_RETRY_COUNT = 3
|
private const val MAX_RETRY_COUNT = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class GiftCommandService(
|
|||||||
val sizePrice = sizePriceRepository.findBySizeCode(command.sizeCode)?.takeIf { it.isActive } ?: throw invalidRequest()
|
val sizePrice = sizePriceRepository.findBySizeCode(command.sizeCode)?.takeIf { it.isActive } ?: throw invalidRequest()
|
||||||
val appliedAt = now()
|
val appliedAt = now()
|
||||||
val trackingDeadlineAt = appliedAt.plusDays(3)
|
val trackingDeadlineAt = appliedAt.plusDays(3)
|
||||||
val applicationNo = applicationNoGenerator.generate(appliedAt)
|
val applicationNo = applicationNoGenerator.generate(category, appliedAt)
|
||||||
val canUsageId = canPaymentService.spendGiftCan(
|
val canUsageId = canPaymentService.spendGiftCan(
|
||||||
senderMemberId,
|
senderMemberId,
|
||||||
sizePrice.salePriceCan,
|
sizePrice.salePriceCan,
|
||||||
|
|||||||
@@ -454,7 +454,15 @@ class GiftCommandServiceTest {
|
|||||||
val giftPushService = Mockito.mock(GiftPushService::class.java)
|
val giftPushService = Mockito.mock(GiftPushService::class.java)
|
||||||
val sender = Member(password = "password", nickname = "sender", container = "aos").apply { id = SENDER_ID }
|
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 creator = Member(password = "password", nickname = "creator").apply { id = RECIPIENT_ID }
|
||||||
val category = GiftCategory("DOLL", "인형", false, true).apply { id = CATEGORY_ID }
|
val category = GiftCategory(
|
||||||
|
classificationNumber = "100",
|
||||||
|
categoryCode = "DOLL",
|
||||||
|
name = "인형",
|
||||||
|
receiptCode = "A",
|
||||||
|
representativeItem = "피규어",
|
||||||
|
requiresDamageWaiver = false,
|
||||||
|
isActive = true
|
||||||
|
).apply { id = CATEGORY_ID }
|
||||||
val sizePrice = GiftSizePrice(GiftSize.SMALL, BASE_PRICE_CAN, SALE_PRICE_CAN, true)
|
val sizePrice = GiftSizePrice(GiftSize.SMALL, BASE_PRICE_CAN, SALE_PRICE_CAN, true)
|
||||||
lateinit var savedGift: Gift
|
lateinit var savedGift: Gift
|
||||||
lateinit var savedDelivery: GiftDelivery
|
lateinit var savedDelivery: GiftDelivery
|
||||||
@@ -486,7 +494,7 @@ class GiftCommandServiceTest {
|
|||||||
setCreatorExists(true)
|
setCreatorExists(true)
|
||||||
Mockito.`when`(categoryRepository.findById(CATEGORY_ID)).thenReturn(Optional.of(category))
|
Mockito.`when`(categoryRepository.findById(CATEGORY_ID)).thenReturn(Optional.of(category))
|
||||||
Mockito.`when`(sizePriceRepository.findBySizeCode(GiftSize.SMALL)).thenReturn(sizePrice)
|
Mockito.`when`(sizePriceRepository.findBySizeCode(GiftSize.SMALL)).thenReturn(sizePrice)
|
||||||
Mockito.`when`(applicationNoGenerator.generate(NOW)).thenReturn(APPLICATION_NO)
|
Mockito.`when`(applicationNoGenerator.generate(category, NOW)).thenReturn(APPLICATION_NO)
|
||||||
Mockito.`when`(canPaymentService.spendGiftCan(SENDER_ID, SALE_PRICE_CAN, "aos")).thenReturn(USE_CAN_ID)
|
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 ->
|
Mockito.`when`(giftRepository.save(Mockito.any(Gift::class.java))).thenAnswer { invocation ->
|
||||||
invocation.getArgument<Gift>(0).apply {
|
invocation.getArgument<Gift>(0).apply {
|
||||||
|
|||||||
+56
-9
@@ -1,7 +1,10 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.gift.application
|
package kr.co.vividnext.sodalive.v2.gift.application
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftApplicationNoSequence
|
||||||
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftApplicationNoSequenceRepository
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftApplicationNoSequenceRepository
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategory
|
||||||
|
import kr.co.vividnext.sodalive.v2.gift.adapter.out.persistence.GiftCategoryRepository
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.DisplayName
|
import org.junit.jupiter.api.DisplayName
|
||||||
@@ -24,24 +27,56 @@ import java.util.concurrent.Executors
|
|||||||
@DisplayName("선물 신청번호 채번 테스트")
|
@DisplayName("선물 신청번호 채번 테스트")
|
||||||
class GiftApplicationNoGeneratorTest @Autowired constructor(
|
class GiftApplicationNoGeneratorTest @Autowired constructor(
|
||||||
private val repository: GiftApplicationNoSequenceRepository,
|
private val repository: GiftApplicationNoSequenceRepository,
|
||||||
|
private val categoryRepository: GiftCategoryRepository,
|
||||||
private val transactionManager: PlatformTransactionManager
|
private val transactionManager: PlatformTransactionManager
|
||||||
) {
|
) {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
repository.deleteAll()
|
repository.deleteAll()
|
||||||
|
categoryRepository.deleteAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("같은 날짜 신청번호를 순서대로 발급한다")
|
@DisplayName("같은 카테고리와 날짜 신청번호를 순서대로 발급한다")
|
||||||
fun shouldGenerateSequentialApplicationNoByDate() {
|
fun shouldGenerateSequentialApplicationNoByCategoryAndDate() {
|
||||||
val generator = GiftApplicationNoGenerator(repository, transactionManager)
|
val generator = GiftApplicationNoGenerator(repository, transactionManager)
|
||||||
val now = LocalDateTime.of(2026, 9, 29, 12, 0)
|
val now = LocalDateTime.of(2026, 9, 29, 12, 0)
|
||||||
|
val category = saveCategory("100", "DOLL", "A")
|
||||||
|
|
||||||
val first = generator.generate(now)
|
val first = generator.generate(category, now)
|
||||||
val second = generator.generate(now)
|
val second = generator.generate(category, now)
|
||||||
|
|
||||||
assertEquals("G20260929000001", first)
|
assertEquals("A-1002609290001", first)
|
||||||
assertEquals("G20260929000002", second)
|
assertEquals("A-1002609290002", second)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("카테고리와 날짜가 다르면 sequence를 따로 발급한다")
|
||||||
|
fun shouldGenerateApplicationNoByCategoryAndDateIndependently() {
|
||||||
|
val generator = GiftApplicationNoGenerator(repository, transactionManager)
|
||||||
|
val doll = saveCategory("100", "DOLL", "A")
|
||||||
|
val food = saveCategory("200", "FOOD", "C")
|
||||||
|
|
||||||
|
val dollFirst = generator.generate(doll, LocalDateTime.of(2026, 9, 29, 12, 0))
|
||||||
|
val foodFirst = generator.generate(food, LocalDateTime.of(2026, 9, 29, 12, 0))
|
||||||
|
val dollNextDay = generator.generate(doll, LocalDateTime.of(2026, 9, 30, 12, 0))
|
||||||
|
|
||||||
|
assertEquals("A-1002609290001", dollFirst)
|
||||||
|
assertEquals("C-2002609290001", foodFirst)
|
||||||
|
assertEquals("A-1002609300001", dollNextDay)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("sequence가 9999를 초과하면 자리수를 늘린다")
|
||||||
|
fun shouldExpandSequenceDigitsAfter9999() {
|
||||||
|
val generator = GiftApplicationNoGenerator(repository, transactionManager)
|
||||||
|
val category = saveCategory("100", "DOLL", "A")
|
||||||
|
val now = LocalDateTime.of(2026, 9, 30, 12, 0)
|
||||||
|
repository.save(GiftApplicationNoSequence(categoryId = category.id!!, sequenceDate = "260930", lastSequence = 9999))
|
||||||
|
|
||||||
|
val applicationNo = generator.generate(category, now)
|
||||||
|
|
||||||
|
assertEquals("A-10026093010000", applicationNo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -49,12 +84,13 @@ class GiftApplicationNoGeneratorTest @Autowired constructor(
|
|||||||
fun shouldGenerateUniqueApplicationNoConcurrently() {
|
fun shouldGenerateUniqueApplicationNoConcurrently() {
|
||||||
val generator = GiftApplicationNoGenerator(repository, transactionManager)
|
val generator = GiftApplicationNoGenerator(repository, transactionManager)
|
||||||
val now = LocalDateTime.of(2026, 9, 29, 12, 0)
|
val now = LocalDateTime.of(2026, 9, 29, 12, 0)
|
||||||
|
val category = saveCategory("100", "DOLL", "A")
|
||||||
val executor = Executors.newFixedThreadPool(4)
|
val executor = Executors.newFixedThreadPool(4)
|
||||||
|
|
||||||
val applicationNos = try {
|
val applicationNos = try {
|
||||||
executor.invokeAll(
|
executor.invokeAll(
|
||||||
(1..10).map {
|
(1..10).map {
|
||||||
Callable { generator.generate(now) }
|
Callable { generator.generate(category, now) }
|
||||||
}
|
}
|
||||||
).map { it.get() }
|
).map { it.get() }
|
||||||
} finally {
|
} finally {
|
||||||
@@ -62,7 +98,18 @@ class GiftApplicationNoGeneratorTest @Autowired constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
assertEquals(10, applicationNos.toSet().size)
|
assertEquals(10, applicationNos.toSet().size)
|
||||||
assertEquals("G20260929000001", applicationNos.minOrNull())
|
assertEquals("A-1002609290001", applicationNos.minOrNull())
|
||||||
assertEquals("G20260929000010", applicationNos.maxOrNull())
|
assertEquals("A-1002609290010", applicationNos.maxOrNull())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveCategory(classificationNumber: String, categoryCode: String, receiptCode: String) =
|
||||||
|
categoryRepository.save(
|
||||||
|
GiftCategory(
|
||||||
|
classificationNumber = classificationNumber,
|
||||||
|
categoryCode = categoryCode,
|
||||||
|
name = "인형",
|
||||||
|
receiptCode = receiptCode,
|
||||||
|
representativeItem = "피규어"
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user