fix(agent-ratio): 에이전트 정산 비율 수정 충돌 처리를 안정화한다

This commit is contained in:
2026-04-11 21:40:45 +09:00
parent 88ffaf6d04
commit 08ba6a6046
3 changed files with 80 additions and 18 deletions

View File

@@ -27,7 +27,7 @@ class AdminAgentSettlementRatioService(
val ratio = request.toEntity()
ratio.member = agent
saveRatioOrThrow(request, ratio)
saveRatioOrThrow(ratio)
}
@Transactional
@@ -39,11 +39,11 @@ class AdminAgentSettlementRatioService(
?: throw SodaException(messageKey = "partner.agent.ratio.not_found")
validateNewEffectiveFrom(existing.effectiveFrom, request.effectiveFrom)
existing.close(request.effectiveFrom)
repository.save(existing)
repository.saveAndFlush(existing)
val ratio = request.toEntity()
ratio.member = agent
saveRatioOrThrow(request, ratio)
saveRatioOrThrow(ratio)
}
@Transactional(readOnly = true)
@@ -95,16 +95,20 @@ class AdminAgentSettlementRatioService(
}
}
private fun saveRatioOrThrow(
request: CreateAgentSettlementRatioRequest,
ratio: AgentSettlementRatio
) {
private fun saveRatioOrThrow(ratio: AgentSettlementRatio) {
try {
repository.saveAndFlush(ratio)
} catch (e: DataIntegrityViolationException) {
repository.findFirstByMemberIdAndEffectiveToIsNull(request.memberId)
?: throw e
throw SodaException(messageKey = "partner.agent.ratio.invalid_effective_from")
if (isActiveRatioConflict(e)) {
throw SodaException(messageKey = "partner.agent.ratio.invalid_effective_from")
}
throw e
}
}
private fun isActiveRatioConflict(exception: Throwable): Boolean {
return generateSequence(exception) { it.cause }
.mapNotNull { it.message }
.any { it.contains("uk_agent_settlement_ratio_member_active") }
}
}

View File

@@ -225,16 +225,15 @@ class AgentSettlementRatioServiceTest {
settlementRatio = 15,
effectiveFrom = LocalDateTime.of(2026, 4, 9, 10, 0)
)
val currentRatio = AgentSettlementRatio(
settlementRatio = 12,
effectiveFrom = LocalDateTime.of(2026, 4, 9, 10, 0)
val duplicateException = DataIntegrityViolationException(
"duplicate",
RuntimeException("Duplicate entry '31-1' for key 'agent_settlement_ratio.uk_agent_settlement_ratio_member_active'")
)
currentRatio.member = agent
Mockito.`when`(memberRepository.findByIdForUpdate(31L)).thenReturn(agent)
Mockito.`when`(repository.findFirstByMemberIdAndEffectiveToIsNull(31L)).thenReturn(null, currentRatio)
Mockito.`when`(repository.findFirstByMemberIdAndEffectiveToIsNull(31L)).thenReturn(null)
Mockito.`when`(repository.saveAndFlush(Mockito.any(AgentSettlementRatio::class.java)))
.thenThrow(DataIntegrityViolationException("duplicate"))
.thenThrow(duplicateException)
val exception = assertThrows(SodaException::class.java) {
service.createAgentSettlementRatio(request)
@@ -286,7 +285,7 @@ class AgentSettlementRatioServiceTest {
}
@Test
@DisplayName("관리자는 기존 에이전트 정산 비율을 수정할 수 있")
@DisplayName("관리자는 기존 활성 비율 종료를 먼저 flush한 뒤 새 에이전트 정산 비율을 수정 저장한")
fun shouldUpdateAgentSettlementRatio() {
val agent = Member(password = "password", nickname = "agent", role = MemberRole.AGENT)
agent.id = 31L
@@ -311,7 +310,7 @@ class AgentSettlementRatioServiceTest {
val ratioCaptor = ArgumentCaptor.forClass(AgentSettlementRatio::class.java)
val inOrder: InOrder = Mockito.inOrder(repository)
inOrder.verify(repository).save(existingRatio)
inOrder.verify(repository).saveAndFlush(existingRatio)
inOrder.verify(repository).saveAndFlush(ratioCaptor.capture())
assertEquals(agent, ratioCaptor.value.member)
@@ -320,6 +319,46 @@ class AgentSettlementRatioServiceTest {
assertEquals(null, ratioCaptor.value.effectiveTo)
}
@Test
@DisplayName("정산 비율 수정 중 활성 unique 제약이 충돌하면 같은 세션 재조회 없이 잘못된 effectiveFrom 예외로 변환한다")
fun shouldThrowWithoutRequeryWhenUpdatingRatioViolatesActiveUniqueConstraint() {
val agent = Member(password = "password", nickname = "agent", role = MemberRole.AGENT)
agent.id = 31L
val existingRatio = AgentSettlementRatio(
settlementRatio = 10,
effectiveFrom = LocalDateTime.of(2026, 4, 1, 0, 0)
)
existingRatio.member = agent
val newEffectiveFrom = LocalDateTime.of(2026, 4, 9, 10, 0)
val request = CreateAgentSettlementRatioRequest(
memberId = 31L,
settlementRatio = 18,
effectiveFrom = newEffectiveFrom
)
val duplicateException = DataIntegrityViolationException(
"duplicate",
RuntimeException("Duplicate entry '31-1' for key 'agent_settlement_ratio.uk_agent_settlement_ratio_member_active'")
)
Mockito.`when`(memberRepository.findByIdForUpdate(31L)).thenReturn(agent)
Mockito.`when`(repository.findFirstByMemberIdAndEffectiveToIsNull(31L)).thenReturn(existingRatio)
Mockito.`when`(repository.saveAndFlush(Mockito.any(AgentSettlementRatio::class.java))).thenAnswer { invocation ->
val ratio = invocation.getArgument<AgentSettlementRatio>(0)
if (ratio === existingRatio) {
ratio
} else {
throw duplicateException
}
}
val exception = assertThrows(SodaException::class.java) {
service.updateAgentSettlementRatio(request)
}
assertEquals("partner.agent.ratio.invalid_effective_from", exception.messageKey)
Mockito.verify(repository, Mockito.times(1)).findFirstByMemberIdAndEffectiveToIsNull(31L)
}
@Test
@DisplayName("기존 정산 비율이 없으면 수정할 수 없다")
fun shouldThrowWhenSettlementRatioDoesNotExist() {