fix(agent-calculate): 에이전트 정산 total projection 조회를 DB 합계 쿼리로 분리한다

This commit is contained in:
2026-04-10 14:30:23 +09:00
parent a661693ea9
commit c0f5c9ca33
4 changed files with 583 additions and 32 deletions

View File

@@ -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(

View File

@@ -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)