fix(agent-settlement): 스냅샷 finalize 집계를 단일 누적으로 정리한다

This commit is contained in:
2026-04-10 13:51:17 +09:00
parent 53f37b93fb
commit 83fdb3400d
2 changed files with 205 additions and 51 deletions

View File

@@ -9,7 +9,6 @@ import kr.co.vividnext.sodalive.partner.agent.calculate.GetAgentChannelDonationS
import kr.co.vividnext.sodalive.partner.agent.calculate.GetAgentChannelDonationSettlementByCreatorQueryData
import kr.co.vividnext.sodalive.partner.agent.calculate.GetAgentCreatorSettlementSummaryQueryData
import kr.co.vividnext.sodalive.partner.agent.calculate.GetAgentSettlementByCreatorItem
import kr.co.vividnext.sodalive.partner.agent.calculate.toMergedResponseItems
import kr.co.vividnext.sodalive.partner.agent.settlement.snapshot.AgentSettlementSnapshot
import kr.co.vividnext.sodalive.partner.agent.settlement.snapshot.AgentSettlementSnapshotRepository
import kr.co.vividnext.sodalive.partner.agent.settlement.snapshot.AgentSettlementSnapshotSourceDetail
@@ -49,6 +48,53 @@ class AdminAgentSettlementSnapshotService(
val sourceDetailsByCreatorId: Map<Long, List<SourceDetailDraft>>
)
private data class SnapshotAggregateDraft(
val creatorId: Long,
val creatorNickname: String,
var assignmentId: Long? = null,
var agentSettlementRatioId: Long? = null,
var appliedAgentSettlementRatio: Int? = null,
var sourceRowCount: Int = 0,
var count: Int = 0,
var totalCan: Int = 0,
var krw: Int = 0,
var fee: Int = 0,
var settlementAmount: Int = 0,
var tax: Int = 0,
var depositAmount: Int = 0,
var agentSettlementAmount: Int = 0,
val sourceDetails: MutableList<SourceDetailDraft> = mutableListOf()
) {
fun add(
assignmentId: Long?,
agentSettlementRatioId: Long?,
appliedAgentSettlementRatio: Int?,
count: Int,
totalCan: Int,
krw: Int,
fee: Int,
settlementAmount: Int,
tax: Int,
depositAmount: Int,
agentSettlementAmount: Int,
sourceDetail: SourceDetailDraft
) {
sourceRowCount += 1
this.assignmentId = if (sourceRowCount == 1) assignmentId else null
this.agentSettlementRatioId = if (sourceRowCount == 1) agentSettlementRatioId else null
this.appliedAgentSettlementRatio = if (sourceRowCount == 1) appliedAgentSettlementRatio else null
this.count += count
this.totalCan += totalCan
this.krw += krw
this.fee += fee
this.settlementAmount += settlementAmount
this.tax += tax
this.depositAmount += depositAmount
this.agentSettlementAmount += agentSettlementAmount
sourceDetails.add(sourceDetail)
}
}
@Transactional
fun finalizeSnapshots(
request: FinalizeAgentSettlementSnapshotRequest,
@@ -136,30 +182,57 @@ class AdminAgentSettlementSnapshotService(
rows: List<GetAgentCreatorSettlementSummaryQueryData>
): SnapshotFinalizeDraft {
val (startDate, endDate) = request.toDateRange()
val rowsByCreator = rows.groupBy { it.creatorId }
val snapshots = rowsByCreator.map { (creatorId, creatorRows) ->
val merged = creatorRows.toMergedResponseItems().single()
val singleSourceRow = creatorRows.singleOrNull()
val aggregateDrafts = linkedMapOf<Long, SnapshotAggregateDraft>()
rows.forEach { row ->
val item = row.toResponseItem()
val sourceDetail = item.toSourceDetailDraft(
assignmentId = row.assignmentId,
agentSettlementRatioId = row.agentSettlementRatioId,
appliedAgentSettlementRatio = row.agentSettlementRatio
)
val aggregate = aggregateDrafts.getOrPut(row.creatorId) {
SnapshotAggregateDraft(
creatorId = row.creatorId,
creatorNickname = item.creatorNickname
)
}
aggregate.add(
assignmentId = row.assignmentId,
agentSettlementRatioId = row.agentSettlementRatioId,
appliedAgentSettlementRatio = row.agentSettlementRatio,
count = item.count,
totalCan = item.totalCan,
krw = item.krw,
fee = item.fee,
settlementAmount = item.settlementAmount,
tax = item.tax,
depositAmount = item.depositAmount,
agentSettlementAmount = item.agentSettlementAmount,
sourceDetail = sourceDetail
)
}
val snapshots = aggregateDrafts.values.map { aggregate ->
AgentSettlementSnapshot(
periodStart = startDate,
periodEnd = endDate,
settlementType = request.settlementType,
agentId = agent.id!!,
agentNickname = agent.nickname,
creatorId = creatorId,
creatorNickname = merged.creatorNickname,
assignmentId = singleSourceRow?.assignmentId,
agentSettlementRatioId = singleSourceRow?.agentSettlementRatioId,
appliedAgentSettlementRatio = singleSourceRow?.agentSettlementRatio,
count = merged.count,
totalCan = merged.totalCan,
krw = merged.krw,
fee = merged.fee,
settlementAmount = merged.settlementAmount,
tax = merged.tax,
depositAmount = merged.depositAmount,
agentSettlementAmount = merged.agentSettlementAmount,
creatorId = aggregate.creatorId,
creatorNickname = aggregate.creatorNickname,
assignmentId = aggregate.assignmentId,
agentSettlementRatioId = aggregate.agentSettlementRatioId,
appliedAgentSettlementRatio = aggregate.appliedAgentSettlementRatio,
count = aggregate.count,
totalCan = aggregate.totalCan,
krw = aggregate.krw,
fee = aggregate.fee,
settlementAmount = aggregate.settlementAmount,
tax = aggregate.tax,
depositAmount = aggregate.depositAmount,
agentSettlementAmount = aggregate.agentSettlementAmount,
finalizedAt = finalizedAt,
finalizedByMemberId = finalizedByMemberId
)
@@ -167,14 +240,8 @@ class AdminAgentSettlementSnapshotService(
return SnapshotFinalizeDraft(
snapshots = snapshots,
sourceDetailsByCreatorId = rowsByCreator.mapValues { (_, creatorRows) ->
creatorRows.map { row ->
row.toResponseItem().toSourceDetailDraft(
assignmentId = row.assignmentId,
agentSettlementRatioId = row.agentSettlementRatioId,
appliedAgentSettlementRatio = row.agentSettlementRatio
)
}
sourceDetailsByCreatorId = aggregateDrafts.mapValues { (_, aggregate) ->
aggregate.sourceDetails.toList()
}
)
}
@@ -187,30 +254,57 @@ class AdminAgentSettlementSnapshotService(
rows: List<GetAgentChannelDonationSettlementByCreatorQueryData>
): SnapshotFinalizeDraft {
val (startDate, endDate) = request.toDateRange()
val rowsByCreator = rows.groupBy { it.creatorId }
val snapshots = rowsByCreator.map { (creatorId, creatorRows) ->
val merged = creatorRows.toMergedResponseItems().single()
val singleSourceRow = creatorRows.singleOrNull()
val aggregateDrafts = linkedMapOf<Long, SnapshotAggregateDraft>()
rows.forEach { row ->
val item = row.toResponseItem()
val sourceDetail = item.toSourceDetailDraft(
assignmentId = row.assignmentId,
agentSettlementRatioId = row.agentSettlementRatioId,
appliedAgentSettlementRatio = row.agentSettlementRatio
)
val aggregate = aggregateDrafts.getOrPut(row.creatorId) {
SnapshotAggregateDraft(
creatorId = row.creatorId,
creatorNickname = item.creatorNickname
)
}
aggregate.add(
assignmentId = row.assignmentId,
agentSettlementRatioId = row.agentSettlementRatioId,
appliedAgentSettlementRatio = row.agentSettlementRatio,
count = item.count,
totalCan = item.totalCan,
krw = item.krw,
fee = item.fee,
settlementAmount = item.settlementAmount,
tax = item.withholdingTax,
depositAmount = item.depositAmount,
agentSettlementAmount = item.agentSettlementAmount,
sourceDetail = sourceDetail
)
}
val snapshots = aggregateDrafts.values.map { aggregate ->
AgentSettlementSnapshot(
periodStart = startDate,
periodEnd = endDate,
settlementType = request.settlementType,
agentId = agent.id!!,
agentNickname = agent.nickname,
creatorId = creatorId,
creatorNickname = merged.creatorNickname,
assignmentId = singleSourceRow?.assignmentId,
agentSettlementRatioId = singleSourceRow?.agentSettlementRatioId,
appliedAgentSettlementRatio = singleSourceRow?.agentSettlementRatio,
count = merged.count,
totalCan = merged.totalCan,
krw = merged.krw,
fee = merged.fee,
settlementAmount = merged.settlementAmount,
tax = merged.withholdingTax,
depositAmount = merged.depositAmount,
agentSettlementAmount = merged.agentSettlementAmount,
creatorId = aggregate.creatorId,
creatorNickname = aggregate.creatorNickname,
assignmentId = aggregate.assignmentId,
agentSettlementRatioId = aggregate.agentSettlementRatioId,
appliedAgentSettlementRatio = aggregate.appliedAgentSettlementRatio,
count = aggregate.count,
totalCan = aggregate.totalCan,
krw = aggregate.krw,
fee = aggregate.fee,
settlementAmount = aggregate.settlementAmount,
tax = aggregate.tax,
depositAmount = aggregate.depositAmount,
agentSettlementAmount = aggregate.agentSettlementAmount,
finalizedAt = finalizedAt,
finalizedByMemberId = finalizedByMemberId
)
@@ -218,14 +312,8 @@ class AdminAgentSettlementSnapshotService(
return SnapshotFinalizeDraft(
snapshots = snapshots,
sourceDetailsByCreatorId = rowsByCreator.mapValues { (_, creatorRows) ->
creatorRows.map { row ->
row.toResponseItem().toSourceDetailDraft(
assignmentId = row.assignmentId,
agentSettlementRatioId = row.agentSettlementRatioId,
appliedAgentSettlementRatio = row.agentSettlementRatio
)
}
sourceDetailsByCreatorId = aggregateDrafts.mapValues { (_, aggregate) ->
aggregate.sourceDetails.toList()
}
)
}

View File

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.admin.partner.agent.settlement
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.member.MemberRole
@@ -13,6 +14,7 @@ import kr.co.vividnext.sodalive.partner.agent.settlement.snapshot.AgentSettlemen
import kr.co.vividnext.sodalive.partner.agent.settlement.snapshot.FinalizeAgentSettlementSnapshotRequest
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
@@ -236,4 +238,68 @@ class AdminAgentSettlementSnapshotServiceTest {
Mockito.verifyNoInteractions(calculateRepository)
Mockito.verifyNoInteractions(memberRepository)
}
@Test
@DisplayName("관리자 finalize는 대상 에이전트 회원이 없으면 실패한다")
fun shouldThrowWhenFinalizingSnapshotsForMissingAgent() {
val request = FinalizeAgentSettlementSnapshotRequest(
agentId = 7L,
settlementType = AgentSettlementSnapshotType.LIVE,
startDateStr = "2026-02-20",
endDateStr = "2026-02-21"
)
val (startDate, endDate) = request.toDateRange()
Mockito.`when`(
snapshotRepository.existsByPeriodStartAndPeriodEndAndSettlementTypeAndAgentId(
startDate,
endDate,
AgentSettlementSnapshotType.LIVE,
7L
)
).thenReturn(false)
Mockito.`when`(memberRepository.findById(7L)).thenReturn(Optional.empty())
val exception = assertThrows(SodaException::class.java) {
service.finalizeSnapshots(request, finalizedByMemberId = 99L)
}
assertEquals("partner.agent.ratio.agent_not_found", exception.messageKey)
Mockito.verifyNoInteractions(calculateRepository)
Mockito.verify(snapshotRepository, Mockito.never()).saveAll(Mockito.anyList())
Mockito.verifyNoInteractions(sourceDetailRepository)
}
@Test
@DisplayName("관리자 finalize는 대상 회원이 AGENT 역할이 아니면 실패한다")
fun shouldThrowWhenFinalizingSnapshotsForNonAgentMember() {
val request = FinalizeAgentSettlementSnapshotRequest(
agentId = 7L,
settlementType = AgentSettlementSnapshotType.LIVE,
startDateStr = "2026-02-20",
endDateStr = "2026-02-21"
)
val invalidAgent = Member(password = "password", nickname = "user-a", role = MemberRole.USER)
invalidAgent.id = 7L
val (startDate, endDate) = request.toDateRange()
Mockito.`when`(
snapshotRepository.existsByPeriodStartAndPeriodEndAndSettlementTypeAndAgentId(
startDate,
endDate,
AgentSettlementSnapshotType.LIVE,
7L
)
).thenReturn(false)
Mockito.`when`(memberRepository.findById(7L)).thenReturn(Optional.of(invalidAgent))
val exception = assertThrows(SodaException::class.java) {
service.finalizeSnapshots(request, finalizedByMemberId = 99L)
}
assertEquals("partner.agent.ratio.invalid_agent", exception.messageKey)
Mockito.verifyNoInteractions(calculateRepository)
Mockito.verify(snapshotRepository, Mockito.never()).saveAll(Mockito.anyList())
Mockito.verifyNoInteractions(sourceDetailRepository)
}
}