diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepository.kt index a17351b3..060a658e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepository.kt @@ -17,9 +17,13 @@ import kr.co.vividnext.sodalive.partner.agent.assignment.QAgentCreatorRelation.a import kr.co.vividnext.sodalive.partner.agent.ratio.QAgentSettlementRatio.agentSettlementRatio import org.springframework.stereotype.Repository import java.time.LocalDateTime +import javax.persistence.EntityManager @Repository -class AgentCalculateQueryRepository(private val queryFactory: JPAQueryFactory) { +class AgentCalculateQueryRepository( + private val queryFactory: JPAQueryFactory, + private val entityManager: EntityManager +) { fun getAssignedCreatorTotalCount(agentId: Long, currentTime: LocalDateTime): Int { return queryFactory .select(agentCreatorRelation.id.count()) @@ -84,6 +88,19 @@ class AgentCalculateQueryRepository(private val queryFactory: JPAQueryFactory) { return getCalculateLiveByCreatorRows(startDate, endDate, agentId, creatorIds = null) } + fun getCalculateLiveByCreatorTotal( + startDate: LocalDateTime, + endDate: LocalDateTime, + agentId: Long + ): GetAgentSettlementByCreatorTotal { + return getGenericSettlementTotal( + subQuery = buildLiveSettlementTotalSubQuery(), + startDate = startDate, + endDate = endDate, + agentId = agentId + ) + } + fun getCalculateLiveByCreator( startDate: LocalDateTime, endDate: LocalDateTime, @@ -148,6 +165,19 @@ class AgentCalculateQueryRepository(private val queryFactory: JPAQueryFactory) { return getCalculateContentByCreatorRows(startDate, endDate, agentId, creatorIds = null) } + fun getCalculateContentByCreatorTotal( + startDate: LocalDateTime, + endDate: LocalDateTime, + agentId: Long + ): GetAgentSettlementByCreatorTotal { + return getGenericSettlementTotal( + subQuery = buildContentSettlementTotalSubQuery(), + startDate = startDate, + endDate = endDate, + agentId = agentId + ) + } + fun getCalculateContentByCreator( startDate: LocalDateTime, endDate: LocalDateTime, @@ -213,6 +243,19 @@ class AgentCalculateQueryRepository(private val queryFactory: JPAQueryFactory) { return getCalculateCommunityByCreatorRows(startDate, endDate, agentId, creatorIds = null) } + fun getCalculateCommunityByCreatorTotal( + startDate: LocalDateTime, + endDate: LocalDateTime, + agentId: Long + ): GetAgentSettlementByCreatorTotal { + return getGenericSettlementTotal( + subQuery = buildCommunitySettlementTotalSubQuery(), + startDate = startDate, + endDate = endDate, + agentId = agentId + ) + } + fun getCalculateCommunityByCreator( startDate: LocalDateTime, endDate: LocalDateTime, @@ -283,6 +326,19 @@ class AgentCalculateQueryRepository(private val queryFactory: JPAQueryFactory) { return getCalculateContentDonationByCreatorRows(startDate, endDate, agentId, creatorIds = null) } + fun getCalculateContentDonationByCreatorTotal( + startDate: LocalDateTime, + endDate: LocalDateTime, + agentId: Long + ): GetAgentSettlementByCreatorTotal { + return getGenericSettlementTotal( + subQuery = buildContentDonationSettlementTotalSubQuery(), + startDate = startDate, + endDate = endDate, + agentId = agentId + ) + } + fun getCalculateContentDonationByCreator( startDate: LocalDateTime, endDate: LocalDateTime, @@ -351,6 +407,65 @@ class AgentCalculateQueryRepository(private val queryFactory: JPAQueryFactory) { return getChannelDonationByCreatorRows(startDate, endDate, agentId, creatorIds = null) } + fun getChannelDonationByCreatorTotal( + startDate: LocalDateTime, + endDate: LocalDateTime, + agentId: Long + ): GetAgentChannelDonationSettlementTotal { + val query = entityManager.createNativeQuery( + """ + SELECT + COALESCE(SUM(grouped.count_value), 0) AS count, + COALESCE(SUM(grouped.total_can), 0) AS totalCan, + COALESCE(SUM(grouped.total_can * 100), 0) AS krw, + COALESCE(SUM(ROUND(grouped.total_can * 100 * 0.066, 0)), 0) AS fee, + COALESCE(SUM(ROUND(((grouped.total_can * 100) - ROUND(grouped.total_can * 100 * 0.066, 0)) * 0.85, 0)), 0) AS settlementAmount, + COALESCE(SUM(ROUND(ROUND(((grouped.total_can * 100) - ROUND(grouped.total_can * 100 * 0.066, 0)) * 0.85, 0) * 0.033, 0)), 0) AS withholdingTax, + COALESCE(SUM(ROUND(((grouped.total_can * 100) - ROUND(grouped.total_can * 100 * 0.066, 0)) * 0.85, 0) - ROUND(ROUND(((grouped.total_can * 100) - ROUND(grouped.total_can * 100 * 0.066, 0)) * 0.85, 0) * 0.033, 0)), 0) AS depositAmount, + COALESCE(SUM(ROUND(ROUND(((grouped.total_can * 100) - ROUND(grouped.total_can * 100 * 0.066, 0)) * 0.85, 0) * (grouped.agent_settlement_ratio / 100.0), 0)), 0) AS agentSettlementAmount + FROM ( + SELECT + COUNT(DISTINCT uc.id) AS count_value, + SUM(ucc.can) AS total_can, + COALESCE(asr.settlement_ratio, 10) AS agent_settlement_ratio + FROM use_can_calculate ucc + INNER JOIN use_can uc ON ucc.use_can_id = uc.id + INNER JOIN member m ON m.id = ucc.recipient_creator_id + INNER JOIN agent_creator_relation acr ON acr.creator_id = m.id + AND acr.agent_id = :agentId + AND acr.assigned_at <= uc.created_at + AND (acr.unassigned_at IS NULL OR acr.unassigned_at > uc.created_at) + LEFT JOIN agent_settlement_ratio asr ON asr.member_id = :agentId + AND asr.effective_from <= uc.created_at + AND (asr.effective_to IS NULL OR asr.effective_to > uc.created_at) + WHERE uc.can_usage = 'CHANNEL_DONATION' + AND uc.is_refund = FALSE + AND ucc.status = 'RECEIVED' + AND uc.created_at >= :startDate + AND uc.created_at <= :endDate + GROUP BY acr.id, asr.id, COALESCE(asr.settlement_ratio, 10) + ) grouped + """.trimIndent() + ) + + query.setParameter("startDate", startDate) + query.setParameter("endDate", endDate) + query.setParameter("agentId", agentId) + + val result = query.singleResult as Array<*> + + return GetAgentChannelDonationSettlementTotal( + count = result[0].toIntValue(), + totalCan = result[1].toIntValue(), + krw = result[2].toIntValue(), + fee = result[3].toIntValue(), + settlementAmount = result[4].toIntValue(), + withholdingTax = result[5].toIntValue(), + depositAmount = result[6].toIntValue(), + agentSettlementAmount = result[7].toIntValue() + ) + } + fun getChannelDonationByCreator( startDate: LocalDateTime, endDate: LocalDateTime, @@ -643,6 +758,156 @@ class AgentCalculateQueryRepository(private val queryFactory: JPAQueryFactory) { .fetch() } + private fun getGenericSettlementTotal( + subQuery: String, + startDate: LocalDateTime, + endDate: LocalDateTime, + agentId: Long + ): GetAgentSettlementByCreatorTotal { + val query = entityManager.createNativeQuery( + """ + SELECT + COALESCE(SUM(grouped.count_value), 0) AS count, + COALESCE(SUM(grouped.total_can), 0) AS totalCan, + COALESCE(SUM(grouped.total_can * 100), 0) AS krw, + COALESCE(SUM(ROUND(grouped.total_can * 100 * 0.066, 0)), 0) AS fee, + COALESCE(SUM(ROUND(((grouped.total_can * 100) - ((grouped.total_can * 100) * 0.066)) * (grouped.settlement_ratio / 100.0), 0)), 0) AS settlementAmount, + COALESCE(SUM(ROUND((((grouped.total_can * 100) - ((grouped.total_can * 100) * 0.066)) * (grouped.settlement_ratio / 100.0)) * 0.033, 0)), 0) AS tax, + COALESCE(SUM(ROUND((((grouped.total_can * 100) - ((grouped.total_can * 100) * 0.066)) * (grouped.settlement_ratio / 100.0)) - ((((grouped.total_can * 100) - ((grouped.total_can * 100) * 0.066)) * (grouped.settlement_ratio / 100.0)) * 0.033), 0)), 0) AS depositAmount, + COALESCE(SUM(ROUND(ROUND(((grouped.total_can * 100) - ((grouped.total_can * 100) * 0.066)) * (grouped.settlement_ratio / 100.0), 0) * (grouped.agent_settlement_ratio / 100.0), 0)), 0) AS agentSettlementAmount + FROM ( + $subQuery + ) grouped + """.trimIndent() + ) + + query.setParameter("startDate", startDate) + query.setParameter("endDate", endDate) + query.setParameter("agentId", agentId) + + val result = query.singleResult as Array<*> + + return GetAgentSettlementByCreatorTotal( + count = result[0].toIntValue(), + totalCan = result[1].toIntValue(), + krw = result[2].toIntValue(), + fee = result[3].toIntValue(), + settlementAmount = result[4].toIntValue(), + tax = result[5].toIntValue(), + depositAmount = result[6].toIntValue(), + agentSettlementAmount = result[7].toIntValue() + ) + } + + private fun buildLiveSettlementTotalSubQuery() = """ + SELECT + COUNT(uc.id) AS count_value, + SUM(uc.can + uc.reward_can) AS total_can, + COALESCE(csr.live_settlement_ratio, 70) AS settlement_ratio, + COALESCE(asr.settlement_ratio, 10) AS agent_settlement_ratio + FROM use_can uc + INNER JOIN live_room lr ON uc.room_id = lr.id + INNER JOIN member m ON lr.member_id = m.id + INNER JOIN agent_creator_relation acr ON acr.creator_id = m.id + AND acr.agent_id = :agentId + AND acr.assigned_at <= uc.created_at + AND (acr.unassigned_at IS NULL OR acr.unassigned_at > uc.created_at) + LEFT JOIN creator_settlement_ratio csr ON csr.member_id = m.id + AND csr.deleted_at IS NULL + LEFT JOIN agent_settlement_ratio asr ON asr.member_id = :agentId + AND asr.effective_from <= uc.created_at + AND (asr.effective_to IS NULL OR asr.effective_to > uc.created_at) + WHERE uc.is_refund = FALSE + AND uc.created_at >= :startDate + AND uc.created_at <= :endDate + GROUP BY acr.id, asr.id, COALESCE(csr.live_settlement_ratio, 70), COALESCE(asr.settlement_ratio, 10) + """.trimIndent() + + private fun buildContentSettlementTotalSubQuery() = """ + SELECT + COUNT(o.id) AS count_value, + SUM(o.can) AS total_can, + COALESCE(COALESCE(c.settlement_ratio, csr.content_settlement_ratio), 70) AS settlement_ratio, + COALESCE(asr.settlement_ratio, 10) AS agent_settlement_ratio + FROM orders o + INNER JOIN content c ON o.content_id = c.id + INNER JOIN member m ON c.member_id = m.id + INNER JOIN agent_creator_relation acr ON acr.creator_id = m.id + AND acr.agent_id = :agentId + AND acr.assigned_at <= o.created_at + AND (acr.unassigned_at IS NULL OR acr.unassigned_at > o.created_at) + LEFT JOIN creator_settlement_ratio csr ON csr.member_id = m.id + AND csr.deleted_at IS NULL + LEFT JOIN agent_settlement_ratio asr ON asr.member_id = :agentId + AND asr.effective_from <= o.created_at + AND (asr.effective_to IS NULL OR asr.effective_to > o.created_at) + WHERE o.created_at >= :startDate + AND o.created_at <= :endDate + AND o.is_active = TRUE + GROUP BY + acr.id, + asr.id, + COALESCE(c.settlement_ratio, csr.content_settlement_ratio), + COALESCE(COALESCE(c.settlement_ratio, csr.content_settlement_ratio), 70), + COALESCE(asr.settlement_ratio, 10) + """.trimIndent() + + private fun buildCommunitySettlementTotalSubQuery() = """ + SELECT + COUNT(uc.id) AS count_value, + SUM(uc.can + uc.reward_can) AS total_can, + COALESCE(csr.community_settlement_ratio, 70) AS settlement_ratio, + COALESCE(asr.settlement_ratio, 10) AS agent_settlement_ratio + FROM use_can uc + INNER JOIN creator_community cc ON uc.creator_community_id = cc.id + INNER JOIN member m ON cc.member_id = m.id + INNER JOIN agent_creator_relation acr ON acr.creator_id = m.id + AND acr.agent_id = :agentId + AND acr.assigned_at <= uc.created_at + AND (acr.unassigned_at IS NULL OR acr.unassigned_at > uc.created_at) + LEFT JOIN creator_settlement_ratio csr ON csr.member_id = m.id + AND csr.deleted_at IS NULL + LEFT JOIN agent_settlement_ratio asr ON asr.member_id = :agentId + AND asr.effective_from <= uc.created_at + AND (asr.effective_to IS NULL OR asr.effective_to > uc.created_at) + WHERE uc.is_refund = FALSE + AND uc.can_usage = 'PAID_COMMUNITY_POST' + AND uc.created_at >= :startDate + AND uc.created_at <= :endDate + GROUP BY acr.id, asr.id, COALESCE(csr.community_settlement_ratio, 70), COALESCE(asr.settlement_ratio, 10) + """.trimIndent() + + private fun buildContentDonationSettlementTotalSubQuery() = """ + SELECT + COUNT(uc.id) AS count_value, + SUM(uc.can + uc.reward_can) AS total_can, + 70 AS settlement_ratio, + COALESCE(asr.settlement_ratio, 10) AS agent_settlement_ratio + FROM use_can uc + INNER JOIN content c ON uc.content_id = c.id + INNER JOIN member m ON c.member_id = m.id + INNER JOIN agent_creator_relation acr ON acr.creator_id = m.id + AND acr.agent_id = :agentId + AND acr.assigned_at <= uc.created_at + AND (acr.unassigned_at IS NULL OR acr.unassigned_at > uc.created_at) + LEFT JOIN agent_settlement_ratio asr ON asr.member_id = :agentId + AND asr.effective_from <= uc.created_at + AND (asr.effective_to IS NULL OR asr.effective_to > uc.created_at) + WHERE uc.is_refund = FALSE + AND uc.can_usage = 'DONATION' + AND uc.created_at >= :startDate + AND uc.created_at <= :endDate + GROUP BY acr.id, asr.id, COALESCE(asr.settlement_ratio, 10) + """.trimIndent() + + private fun Any?.toIntValue(): Int { + return when (this) { + null -> 0 + is Number -> toInt() + else -> this.toString().toInt() + } + } + private fun assignedToAgentAtEventTime( agentId: Long, eventTime: DateTimeExpression diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateService.kt index b50d2ca4..9dcb1495 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateService.kt @@ -40,8 +40,8 @@ class AgentCalculateService( totalCountLoader = { startDate, endDate -> repository.getCalculateLiveByCreatorTotalCount(startDate, endDate, agentId) }, - totalRowsLoader = { startDate, endDate -> - repository.getCalculateLiveByCreator(startDate, endDate, agentId) + totalLoader = { startDate, endDate -> + repository.getCalculateLiveByCreatorTotal(startDate, endDate, agentId) }, pagedRowsLoader = { startDate, endDate -> repository.getCalculateLiveByCreator(startDate, endDate, agentId, offset, limit) @@ -67,8 +67,8 @@ class AgentCalculateService( totalCountLoader = { startDate, endDate -> repository.getCalculateContentByCreatorTotalCount(startDate, endDate, agentId) }, - totalRowsLoader = { startDate, endDate -> - repository.getCalculateContentByCreator(startDate, endDate, agentId) + totalLoader = { startDate, endDate -> + repository.getCalculateContentByCreatorTotal(startDate, endDate, agentId) }, pagedRowsLoader = { startDate, endDate -> repository.getCalculateContentByCreator(startDate, endDate, agentId, offset, limit) @@ -94,8 +94,8 @@ class AgentCalculateService( totalCountLoader = { startDate, endDate -> repository.getCalculateCommunityByCreatorTotalCount(startDate, endDate, agentId) }, - totalRowsLoader = { startDate, endDate -> - repository.getCalculateCommunityByCreator(startDate, endDate, agentId) + totalLoader = { startDate, endDate -> + repository.getCalculateCommunityByCreatorTotal(startDate, endDate, agentId) }, pagedRowsLoader = { startDate, endDate -> repository.getCalculateCommunityByCreator(startDate, endDate, agentId, offset, limit) @@ -121,8 +121,8 @@ class AgentCalculateService( totalCountLoader = { startDate, endDate -> repository.getCalculateContentDonationByCreatorTotalCount(startDate, endDate, agentId) }, - totalRowsLoader = { startDate, endDate -> - repository.getCalculateContentDonationByCreator(startDate, endDate, agentId) + totalLoader = { startDate, endDate -> + repository.getCalculateContentDonationByCreatorTotal(startDate, endDate, agentId) }, pagedRowsLoader = { startDate, endDate -> repository.getCalculateContentDonationByCreator(startDate, endDate, agentId, offset, limit) @@ -156,9 +156,7 @@ class AgentCalculateService( } val totalCount = repository.getChannelDonationByCreatorTotalCount(startDate, endDate, agentId) - val total = repository.getChannelDonationByCreator(startDate, endDate, agentId) - .toMergedResponseItems() - .toResponseTotal() + val total = repository.getChannelDonationByCreatorTotal(startDate, endDate, agentId) val items = repository.getChannelDonationByCreator(startDate, endDate, agentId, offset, limit) .toMergedResponseItems() @@ -177,7 +175,7 @@ class AgentCalculateService( offset: Long, limit: Long, totalCountLoader: (LocalDateTime, LocalDateTime) -> Int, - totalRowsLoader: (LocalDateTime, LocalDateTime) -> List, + totalLoader: (LocalDateTime, LocalDateTime) -> GetAgentSettlementByCreatorTotal, pagedRowsLoader: (LocalDateTime, LocalDateTime) -> List ): GetAgentSettlementByCreatorResponse { val (startDate, endDate) = toDateRange(startDateStr, endDateStr) @@ -198,8 +196,7 @@ class AgentCalculateService( } val totalCount = totalCountLoader(startDate, endDate) - val total = totalRowsLoader(startDate, endDate) - .toResponseTotal() + val total = totalLoader(startDate, endDate) val items = pagedRowsLoader(startDate, endDate) .toMergedResponseItems() diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepositoryTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepositoryTest.kt index 5c9a5911..ce66edd9 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepositoryTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateQueryRepositoryTest.kt @@ -62,7 +62,7 @@ class AgentCalculateQueryRepositoryTest @Autowired constructor( @BeforeEach fun setup() { registerMysqlDateFunctions() - repository = AgentCalculateQueryRepository(queryFactory) + repository = AgentCalculateQueryRepository(queryFactory, entityManager) service = AgentCalculateService(repository, snapshotRepository) } @@ -175,6 +175,57 @@ class AgentCalculateQueryRepositoryTest @Autowired constructor( assertEquals(listOf(creator.id!!, creator.id!!), rows.map { it.creatorId }) } + @Test + @DisplayName("콘텐츠 total projection은 콘텐츠별 비율과 fallback 비율이 섞여도 기존 Kotlin total과 같아야 한다") + fun shouldMatchDbTotalProjectionForContentRowsSplitByEffectiveSettlementRatio() { + val agent = saveMember("agent-content-total", MemberRole.AGENT) + val creator = saveMember("creator-content-total", MemberRole.CREATOR) + val buyer = saveMember("buyer-content-total", MemberRole.USER) + + saveRelation(agent, creator) + saveCreatorSettlementRatio(creator, live = 70, content = 60, community = 70) + + val paidContent = saveAudioContent(creator, "content-total-a", price = 50, settlementRatio = 80) + val fallbackContent = saveAudioContent(creator, "content-total-b", price = 30, settlementRatio = null) + + saveOrder(buyer, creator, paidContent, LocalDateTime.of(2026, 2, 20, 9, 0, 0)) + saveOrder(buyer, creator, fallbackContent, LocalDateTime.of(2026, 2, 20, 10, 0, 0)) + + val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0) + val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59) + + val kotlinTotal = repository.getCalculateContentByCreator(startDate, endDate, agent.id!!).toResponseTotal() + val dbTotal = repository.getCalculateContentByCreatorTotal(startDate, endDate, agent.id!!) + + assertEquals(kotlinTotal, dbTotal) + } + + @Test + @DisplayName("콘텐츠 total projection은 explicit 70과 null fallback 70이 섞여도 기존 Kotlin total과 같아야 한다") + fun shouldMatchDbTotalProjectionWhenExplicitAndFallbackSeventyMustStaySeparated() { + val agent = saveMember("agent-content-fallback-total", MemberRole.AGENT) + val creator = saveMember("creator-content-fallback-total", MemberRole.CREATOR) + val buyer = saveMember("buyer-content-fallback-total", MemberRole.USER) + + saveRelation(agent, creator) + + val explicitRatioContent = saveAudioContent(creator, "content-explicit-seventy", price = 1, settlementRatio = 70) + val fallbackRatioContent = saveAudioContent(creator, "content-fallback-seventy", price = 1, settlementRatio = null) + + saveOrder(buyer, creator, explicitRatioContent, LocalDateTime.of(2026, 2, 20, 9, 0, 0)) + saveOrder(buyer, creator, fallbackRatioContent, LocalDateTime.of(2026, 2, 20, 10, 0, 0)) + + val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0) + val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59) + + val rows = repository.getCalculateContentByCreator(startDate, endDate, agent.id!!) + val kotlinTotal = rows.toResponseTotal() + val dbTotal = repository.getCalculateContentByCreatorTotal(startDate, endDate, agent.id!!) + + assertEquals(2, rows.size) + assertEquals(kotlinTotal, dbTotal) + } + @Test @DisplayName("커뮤니티 크리에이터별 조회는 소속된 크리에이터만 집계한다") fun shouldGetCommunitySummaryRowsOnlyForAssignedCreators() { @@ -269,6 +320,42 @@ class AgentCalculateQueryRepositoryTest @Autowired constructor( assertEquals(50, rows[0].totalCan) } + @Test + @DisplayName("채널후원 total projection은 분할 정산과 agent 비율 이력이 섞여도 기존 Kotlin total과 같아야 한다") + fun shouldMatchDbTotalProjectionForChannelDonationWithSplitCalculatesAndRatioHistory() { + val agent = saveMember("agent-channel-total", MemberRole.AGENT) + val creator = saveMember("creator-channel-total", MemberRole.CREATOR) + val sender = saveMember("sender-channel-total", MemberRole.USER) + + saveRelation(agent, creator) + saveAgentSettlementRatio(agent, settlementRatio = 10, effectiveFrom = LocalDateTime.of(2026, 2, 1, 0, 0, 0)) + saveAgentSettlementRatio( + agent, + settlementRatio = 20, + effectiveFrom = LocalDateTime.of(2026, 2, 20, 12, 0, 0), + effectiveTo = null, + previousEffectiveTo = LocalDateTime.of(2026, 2, 20, 12, 0, 0) + ) + + val beforeRatioUseCan = saveChannelDonationUseCan(sender, 50, LocalDateTime.of(2026, 2, 20, 9, 0, 0)) + saveUseCanCalculate(beforeRatioUseCan, creator.id!!, 20, PaymentGateway.PG) + saveUseCanCalculate(beforeRatioUseCan, creator.id!!, 30, PaymentGateway.GOOGLE_IAP) + + val afterRatioUseCan = saveChannelDonationUseCan(sender, 70, LocalDateTime.of(2026, 2, 20, 14, 0, 0)) + saveUseCanCalculate(afterRatioUseCan, creator.id!!, 40, PaymentGateway.PG) + saveUseCanCalculate(afterRatioUseCan, creator.id!!, 30, PaymentGateway.APPLE_IAP) + + val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0) + val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59) + + val kotlinTotal = repository.getChannelDonationByCreator(startDate, endDate, agent.id!!) + .toMergedResponseItems() + .toResponseTotal() + val dbTotal = repository.getChannelDonationByCreatorTotal(startDate, endDate, agent.id!!) + + assertEquals(kotlinTotal, dbTotal) + } + @Test @DisplayName("페이지 대상 creator가 없으면 모든 카테고리 조회는 빈 rows를 반환한다") fun shouldReturnEmptyRowsWhenPagedCreatorSelectionIsEmptyAcrossAllCategories() { @@ -517,6 +604,85 @@ class AgentCalculateQueryRepositoryTest @Autowired constructor( ) } + @Test + @DisplayName("generic 4종 total projection은 agent 비율 이력으로 row가 갈려도 기존 Kotlin total과 같아야 한다") + fun shouldMatchDbTotalProjectionAcrossAllGenericCategoriesWhenAgentRatioHistorySplitsRows() { + val agent = saveMember("agent-total-ratio-history", MemberRole.AGENT) + val creator = saveMember("creator-total-ratio-history", MemberRole.CREATOR) + val buyer = saveMember("buyer-total-ratio-history", MemberRole.USER) + + saveRelation(agent = agent, creator = creator, assignedAt = LocalDateTime.of(2026, 2, 1, 0, 0, 0), unassignedAt = null) + saveAgentSettlementRatio(agent, settlementRatio = 10, effectiveFrom = LocalDateTime.of(2026, 2, 1, 0, 0, 0)) + saveAgentSettlementRatio( + agent, + settlementRatio = 20, + effectiveFrom = LocalDateTime.of(2026, 2, 20, 12, 0, 0), + effectiveTo = null, + previousEffectiveTo = LocalDateTime.of(2026, 2, 20, 12, 0, 0) + ) + saveCreatorSettlementRatio(creator, live = 70, content = 70, community = 70) + + val room = saveLiveRoom(creator) + saveLiveUseCan(buyer, room, 10, LocalDateTime.of(2026, 2, 20, 10, 0, 0)) + saveLiveUseCan(buyer, room, 20, LocalDateTime.of(2026, 2, 20, 14, 0, 0)) + + val contentBefore = saveAudioContent(creator, "total-ratio-content-before", price = 10, settlementRatio = null) + val contentAfter = saveAudioContent(creator, "total-ratio-content-after", price = 20, settlementRatio = null) + saveOrder(buyer, creator, contentBefore, LocalDateTime.of(2026, 2, 20, 10, 0, 0)) + saveOrder(buyer, creator, contentAfter, LocalDateTime.of(2026, 2, 20, 14, 0, 0)) + + val communityBefore = saveCommunityPost(creator, 10) + val communityAfter = saveCommunityPost(creator, 20) + saveCommunityUseCan(buyer, communityBefore, 10, LocalDateTime.of(2026, 2, 20, 10, 0, 0)) + saveCommunityUseCan(buyer, communityAfter, 20, LocalDateTime.of(2026, 2, 20, 14, 0, 0)) + + saveContentDonationUseCan(buyer, contentBefore, 10, LocalDateTime.of(2026, 2, 20, 10, 0, 0)) + saveContentDonationUseCan(buyer, contentAfter, 20, LocalDateTime.of(2026, 2, 20, 14, 0, 0)) + + val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0) + val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59) + + assertEquals( + repository.getCalculateLiveByCreator(startDate, endDate, agent.id!!).toResponseTotal(), + repository.getCalculateLiveByCreatorTotal(startDate, endDate, agent.id!!) + ) + assertEquals( + repository.getCalculateContentByCreator(startDate, endDate, agent.id!!).toResponseTotal(), + repository.getCalculateContentByCreatorTotal(startDate, endDate, agent.id!!) + ) + assertEquals( + repository.getCalculateCommunityByCreator(startDate, endDate, agent.id!!).toResponseTotal(), + repository.getCalculateCommunityByCreatorTotal(startDate, endDate, agent.id!!) + ) + assertEquals( + repository.getCalculateContentDonationByCreator(startDate, endDate, agent.id!!).toResponseTotal(), + repository.getCalculateContentDonationByCreatorTotal(startDate, endDate, agent.id!!) + ) + } + + @Test + @DisplayName("generic 4종 total projection은 결과가 없으면 0 total을 반환한다") + fun shouldReturnZeroTotalsWhenNoGenericRowsExist() { + val agent = saveMember("agent-total-empty", MemberRole.AGENT) + val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0) + val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59) + val zeroTotal = GetAgentSettlementByCreatorTotal( + count = 0, + totalCan = 0, + krw = 0, + fee = 0, + settlementAmount = 0, + tax = 0, + depositAmount = 0, + agentSettlementAmount = 0 + ) + + assertEquals(zeroTotal, repository.getCalculateLiveByCreatorTotal(startDate, endDate, agent.id!!)) + assertEquals(zeroTotal, repository.getCalculateContentByCreatorTotal(startDate, endDate, agent.id!!)) + assertEquals(zeroTotal, repository.getCalculateCommunityByCreatorTotal(startDate, endDate, agent.id!!)) + assertEquals(zeroTotal, repository.getCalculateContentDonationByCreatorTotal(startDate, endDate, agent.id!!)) + } + private fun saveMember(nickname: String, role: MemberRole): Member { return memberRepository.saveAndFlush( Member( diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateServiceTest.kt index 1cfdd74a..4bdafdfa 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/partner/agent/calculate/AgentCalculateServiceTest.kt @@ -100,12 +100,23 @@ class AgentCalculateServiceTest { ) ).thenReturn(1) Mockito.`when`( - repository.getCalculateLiveByCreator( + repository.getCalculateLiveByCreatorTotal( startDate = "2026-02-20".convertLocalDateTime(), endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59), agentId = 7L ) - ).thenReturn(queryData) + ).thenReturn( + GetAgentSettlementByCreatorTotal( + count = 2, + totalCan = 100, + krw = 10_000, + fee = 660, + settlementAmount = 6_538, + tax = 216, + depositAmount = 6_322, + agentSettlementAmount = 654 + ) + ) Mockito.`when`( repository.getCalculateLiveByCreator( startDate = "2026-02-20".convertLocalDateTime(), @@ -131,6 +142,16 @@ class AgentCalculateServiceTest { assertEquals(654, response.total.agentSettlementAmount) assertEquals(21L, response.items[0].creatorId) assertEquals(654, response.items[0].agentSettlementAmount) + Mockito.verify(repository).getCalculateLiveByCreatorTotal( + "2026-02-20".convertLocalDateTime(), + "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59), + 7L + ) + Mockito.verify(repository, Mockito.never()).getCalculateLiveByCreator( + "2026-02-20".convertLocalDateTime(), + "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59), + 7L + ) } @Test @@ -163,12 +184,23 @@ class AgentCalculateServiceTest { ) ).thenReturn(1) Mockito.`when`( - repository.getCalculateContentByCreator( + repository.getCalculateContentByCreatorTotal( startDate = "2026-02-20".convertLocalDateTime(), endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59), agentId = 7L ) - ).thenReturn(totalRows) + ).thenReturn( + GetAgentSettlementByCreatorTotal( + count = 3, + totalCan = 80, + krw = 8_000, + fee = 528, + settlementAmount = 5_417, + tax = 179, + depositAmount = 5_238, + agentSettlementAmount = 710 + ) + ) Mockito.`when`( repository.getCalculateContentByCreator( startDate = "2026-02-20".convertLocalDateTime(), @@ -219,12 +251,23 @@ class AgentCalculateServiceTest { ) ).thenReturn(1) Mockito.`when`( - repository.getCalculateCommunityByCreator( + repository.getCalculateCommunityByCreatorTotal( startDate = "2026-02-20".convertLocalDateTime(), endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59), agentId = 7L ) - ).thenReturn(queryData) + ).thenReturn( + GetAgentSettlementByCreatorTotal( + count = 2, + totalCan = 30, + krw = 3_000, + fee = 198, + settlementAmount = 1_681, + tax = 55, + depositAmount = 1_626, + agentSettlementAmount = 168 + ) + ) Mockito.`when`( repository.getCalculateCommunityByCreator( startDate = "2026-02-20".convertLocalDateTime(), @@ -271,12 +314,23 @@ class AgentCalculateServiceTest { ) ).thenReturn(1) Mockito.`when`( - repository.getCalculateContentDonationByCreator( + repository.getCalculateContentDonationByCreatorTotal( startDate = "2026-02-20".convertLocalDateTime(), endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59), agentId = 7L ) - ).thenReturn(queryData) + ).thenReturn( + GetAgentSettlementByCreatorTotal( + count = 2, + totalCan = 20, + krw = 2_000, + fee = 132, + settlementAmount = 1_308, + tax = 43, + depositAmount = 1_265, + agentSettlementAmount = 131 + ) + ) Mockito.`when`( repository.getCalculateContentDonationByCreator( startDate = "2026-02-20".convertLocalDateTime(), @@ -323,12 +377,23 @@ class AgentCalculateServiceTest { ) ).thenReturn(1) Mockito.`when`( - repository.getCalculateContentDonationByCreator( + repository.getCalculateContentDonationByCreatorTotal( startDate = "2026-02-20".convertLocalDateTime(), endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59), agentId = 7L ) - ).thenReturn(queryData) + ).thenReturn( + GetAgentSettlementByCreatorTotal( + count = 2, + totalCan = 20, + krw = 2_000, + fee = 132, + settlementAmount = 1_308, + tax = 43, + depositAmount = 1_265, + agentSettlementAmount = 131 + ) + ) Mockito.`when`( repository.getCalculateContentDonationByCreator( startDate = "2026-02-20".convertLocalDateTime(), @@ -374,12 +439,23 @@ class AgentCalculateServiceTest { ) ).thenReturn(1) Mockito.`when`( - repository.getChannelDonationByCreator( + repository.getChannelDonationByCreatorTotal( startDate = "2026-02-20".convertLocalDateTime(), endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59), agentId = 7L ) - ).thenReturn(queryData) + ).thenReturn( + GetAgentChannelDonationSettlementTotal( + count = 1, + totalCan = 50, + krw = 5_000, + fee = 330, + settlementAmount = 3_970, + withholdingTax = 131, + depositAmount = 3_839, + agentSettlementAmount = 397 + ) + ) Mockito.`when`( repository.getChannelDonationByCreator( startDate = "2026-02-20".convertLocalDateTime(), @@ -402,6 +478,16 @@ class AgentCalculateServiceTest { assertEquals(3_970, response.total.settlementAmount) assertEquals(397, response.total.agentSettlementAmount) assertEquals(397, response.items[0].agentSettlementAmount) + Mockito.verify(repository).getChannelDonationByCreatorTotal( + "2026-02-20".convertLocalDateTime(), + "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59), + 7L + ) + Mockito.verify(repository, Mockito.never()).getChannelDonationByCreator( + "2026-02-20".convertLocalDateTime(), + "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59), + 7L + ) } @Test @@ -425,12 +511,23 @@ class AgentCalculateServiceTest { ) ).thenReturn(1) Mockito.`when`( - repository.getChannelDonationByCreator( + repository.getChannelDonationByCreatorTotal( startDate = "2026-02-20".convertLocalDateTime(), endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59), agentId = 7L ) - ).thenReturn(queryData) + ).thenReturn( + GetAgentChannelDonationSettlementTotal( + count = 1, + totalCan = 50, + krw = 5_000, + fee = 330, + settlementAmount = 3_970, + withholdingTax = 131, + depositAmount = 3_839, + agentSettlementAmount = 397 + ) + ) Mockito.`when`( repository.getChannelDonationByCreator( startDate = "2026-02-20".convertLocalDateTime(), @@ -604,7 +701,20 @@ class AgentCalculateServiceTest { val endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59) Mockito.`when`(repository.getCalculateLiveByCreatorTotalCount(startDate, endDate, 7L)).thenReturn(0) - Mockito.`when`(repository.getCalculateLiveByCreator(startDate, endDate, 7L)).thenReturn(emptyList()) + Mockito.`when`( + repository.getCalculateLiveByCreatorTotal(startDate, endDate, 7L) + ).thenReturn( + GetAgentSettlementByCreatorTotal( + count = 0, + totalCan = 0, + krw = 0, + fee = 0, + settlementAmount = 0, + tax = 0, + depositAmount = 0, + agentSettlementAmount = 0 + ) + ) Mockito.`when`(repository.getCalculateLiveByCreator(startDate, endDate, 7L, 0L, 20L)).thenReturn(emptyList()) val response = service.getCalculateLiveByCreator("2026-02-20", "2026-02-21", 7L, 0L, 20L) @@ -628,7 +738,20 @@ class AgentCalculateServiceTest { val endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59) Mockito.`when`(repository.getChannelDonationByCreatorTotalCount(startDate, endDate, 7L)).thenReturn(0) - Mockito.`when`(repository.getChannelDonationByCreator(startDate, endDate, 7L)).thenReturn(emptyList()) + Mockito.`when`( + repository.getChannelDonationByCreatorTotal(startDate, endDate, 7L) + ).thenReturn( + GetAgentChannelDonationSettlementTotal( + count = 0, + totalCan = 0, + krw = 0, + fee = 0, + settlementAmount = 0, + withholdingTax = 0, + depositAmount = 0, + agentSettlementAmount = 0 + ) + ) Mockito.`when`(repository.getChannelDonationByCreator(startDate, endDate, 7L, 0L, 20L)).thenReturn(emptyList()) val response = service.getChannelDonationByCreator("2026-02-20", "2026-02-21", 7L, 0L, 20L)