From 83fdb3400dc2fee7472e68aee7b43bc51a224e46 Mon Sep 17 00:00:00 2001 From: Klaus Date: Fri, 10 Apr 2026 13:51:17 +0900 Subject: [PATCH] =?UTF-8?q?fix(agent-settlement):=20=EC=8A=A4=EB=83=85?= =?UTF-8?q?=EC=83=B7=20finalize=20=EC=A7=91=EA=B3=84=EB=A5=BC=20=EB=8B=A8?= =?UTF-8?q?=EC=9D=BC=20=EB=88=84=EC=A0=81=EC=9C=BC=EB=A1=9C=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AdminAgentSettlementSnapshotService.kt | 190 +++++++++++++----- ...AdminAgentSettlementSnapshotServiceTest.kt | 66 ++++++ 2 files changed, 205 insertions(+), 51 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotService.kt index 6da6cb9a..9c1590e5 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotService.kt @@ -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> ) + 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 = 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 ): 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() + 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 ): 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() + 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() } ) } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotServiceTest.kt index 18d100ea..ebef5af4 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/admin/partner/agent/settlement/AdminAgentSettlementSnapshotServiceTest.kt @@ -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) + } }