feat(agent-calculate): 에이전트별 정산 조회 기능을 추가한다

This commit is contained in:
2026-04-10 02:24:08 +09:00
parent 9e4cd1bb6e
commit bf67dab6a4
10 changed files with 2984 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
package kr.co.vividnext.sodalive.partner.agent.calculate
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@PreAuthorize("hasRole('AGENT')")
@RequestMapping("/agent/calculate")
class AgentCalculateController(private val service: AgentCalculateService) {
@GetMapping("/creator/list")
fun getAssignedCreators(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
val agent = member ?: throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(
service.getAssignedCreators(
agentId = agent.id!!,
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
}
@GetMapping("/live-by-creator")
fun getCalculateLiveByCreator(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
val agent = member ?: throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(
service.getCalculateLiveByCreator(
startDateStr = startDateStr,
endDateStr = endDateStr,
agentId = agent.id!!,
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
}
@GetMapping("/content-by-creator")
fun getCalculateContentByCreator(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
val agent = member ?: throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(
service.getCalculateContentByCreator(
startDateStr = startDateStr,
endDateStr = endDateStr,
agentId = agent.id!!,
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
}
@GetMapping("/community-by-creator")
fun getCalculateCommunityByCreator(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
val agent = member ?: throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(
service.getCalculateCommunityByCreator(
startDateStr = startDateStr,
endDateStr = endDateStr,
agentId = agent.id!!,
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
}
@GetMapping("/channel-donation-by-creator")
fun getChannelDonationByCreator(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
val agent = member ?: throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(
service.getChannelDonationByCreator(
startDateStr = startDateStr,
endDateStr = endDateStr,
agentId = agent.id!!,
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
}
@GetMapping("/content-donation-by-creator")
fun getCalculateContentDonationByCreator(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
val agent = member ?: throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(
service.getCalculateContentDonationByCreator(
startDateStr = startDateStr,
endDateStr = endDateStr,
agentId = agent.id!!,
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
}
}

View File

@@ -0,0 +1,666 @@
package kr.co.vividnext.sodalive.partner.agent.calculate
import com.querydsl.core.types.dsl.DateTimeExpression
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.admin.calculate.ratio.QCreatorSettlementRatio.creatorSettlementRatio
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
import kr.co.vividnext.sodalive.can.use.QUseCanCalculate.useCanCalculate
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.content.order.QOrder.order
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.QCreatorCommunity.creatorCommunity
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.partner.agent.assignment.QAgentCreatorRelation.agentCreatorRelation
import kr.co.vividnext.sodalive.partner.agent.ratio.QAgentSettlementRatio.agentSettlementRatio
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@Repository
class AgentCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
fun getAssignedCreatorTotalCount(agentId: Long, currentTime: LocalDateTime): Int {
return queryFactory
.select(agentCreatorRelation.id.count())
.from(agentCreatorRelation)
.where(
assignedToAgentAtCurrentTime(agentId = agentId, currentTime = currentTime)
)
.fetchOne()
?.toInt()
?: 0
}
fun getAssignedCreators(
agentId: Long,
offset: Long,
limit: Long,
currentTime: LocalDateTime
): List<GetAgentAssignedCreatorItem> {
return queryFactory
.select(
QGetAgentAssignedCreatorItem(
agentCreatorRelation.creator.id,
agentCreatorRelation.creator.nickname
)
)
.from(agentCreatorRelation)
.where(
assignedToAgentAtCurrentTime(agentId = agentId, currentTime = currentTime)
)
.orderBy(agentCreatorRelation.creator.id.desc())
.offset(offset)
.limit(limit)
.fetch()
}
fun getCalculateLiveByCreatorTotalCount(startDate: LocalDateTime, endDate: LocalDateTime, agentId: Long): Int {
return queryFactory
.select(member.id.countDistinct())
.from(useCan)
.innerJoin(useCan.room, liveRoom)
.innerJoin(liveRoom.member, member)
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, useCan.createdAt))
)
.where(
useCan.isRefund.isFalse
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.fetchOne()
?.toInt()
?: 0
}
fun getCalculateLiveByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long
): List<GetAgentCreatorSettlementSummaryQueryData> {
return getCalculateLiveByCreatorRows(startDate, endDate, agentId, creatorIds = null)
}
fun getCalculateLiveByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long,
offset: Long,
limit: Long
): List<GetAgentCreatorSettlementSummaryQueryData> {
val creatorIds = queryFactory
.select(member.id)
.from(useCan)
.innerJoin(useCan.room, liveRoom)
.innerJoin(liveRoom.member, member)
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, useCan.createdAt))
)
.where(
useCan.isRefund.isFalse
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.groupBy(member.id)
.orderBy(member.id.desc())
.offset(offset)
.limit(limit)
.fetch()
if (creatorIds.isEmpty()) {
return emptyList()
}
return getCalculateLiveByCreatorRows(startDate, endDate, agentId, creatorIds)
}
fun getCalculateContentByCreatorTotalCount(startDate: LocalDateTime, endDate: LocalDateTime, agentId: Long): Int {
return queryFactory
.select(member.id.countDistinct())
.from(order)
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, order.createdAt))
)
.where(
order.createdAt.goe(startDate)
.and(order.createdAt.loe(endDate))
.and(order.isActive.isTrue)
)
.fetchOne()
?.toInt()
?: 0
}
fun getCalculateContentByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long
): List<GetAgentCreatorSettlementSummaryQueryData> {
return getCalculateContentByCreatorRows(startDate, endDate, agentId, creatorIds = null)
}
fun getCalculateContentByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long,
offset: Long,
limit: Long
): List<GetAgentCreatorSettlementSummaryQueryData> {
val creatorIds = queryFactory
.select(member.id)
.from(order)
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, order.createdAt))
)
.where(
order.createdAt.goe(startDate)
.and(order.createdAt.loe(endDate))
.and(order.isActive.isTrue)
)
.groupBy(member.id)
.orderBy(member.id.desc())
.offset(offset)
.limit(limit)
.fetch()
if (creatorIds.isEmpty()) {
return emptyList()
}
return getCalculateContentByCreatorRows(startDate, endDate, agentId, creatorIds)
}
fun getCalculateCommunityByCreatorTotalCount(startDate: LocalDateTime, endDate: LocalDateTime, agentId: Long): Int {
return queryFactory
.select(member.id.countDistinct())
.from(useCan)
.innerJoin(useCan.communityPost, creatorCommunity)
.innerJoin(creatorCommunity.member, member)
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, useCan.createdAt))
)
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.fetchOne()
?.toInt()
?: 0
}
fun getCalculateCommunityByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long
): List<GetAgentCreatorSettlementSummaryQueryData> {
return getCalculateCommunityByCreatorRows(startDate, endDate, agentId, creatorIds = null)
}
fun getCalculateCommunityByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long,
offset: Long,
limit: Long
): List<GetAgentCreatorSettlementSummaryQueryData> {
val creatorIds = queryFactory
.select(member.id)
.from(useCan)
.innerJoin(useCan.communityPost, creatorCommunity)
.innerJoin(creatorCommunity.member, member)
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, useCan.createdAt))
)
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.groupBy(member.id)
.orderBy(member.id.desc())
.offset(offset)
.limit(limit)
.fetch()
if (creatorIds.isEmpty()) {
return emptyList()
}
return getCalculateCommunityByCreatorRows(startDate, endDate, agentId, creatorIds)
}
fun getCalculateContentDonationByCreatorTotalCount(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long
): Int {
return queryFactory
.select(member.id.countDistinct())
.from(useCan)
.innerJoin(useCan.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, useCan.createdAt))
)
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.DONATION))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.fetchOne()
?.toInt()
?: 0
}
fun getCalculateContentDonationByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long
): List<GetAgentCreatorSettlementSummaryQueryData> {
return getCalculateContentDonationByCreatorRows(startDate, endDate, agentId, creatorIds = null)
}
fun getCalculateContentDonationByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long,
offset: Long,
limit: Long
): List<GetAgentCreatorSettlementSummaryQueryData> {
val creatorIds = queryFactory
.select(member.id)
.from(useCan)
.innerJoin(useCan.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, useCan.createdAt))
)
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.DONATION))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.groupBy(member.id)
.orderBy(member.id.desc())
.offset(offset)
.limit(limit)
.fetch()
if (creatorIds.isEmpty()) {
return emptyList()
}
return getCalculateContentDonationByCreatorRows(startDate, endDate, agentId, creatorIds)
}
fun getChannelDonationByCreatorTotalCount(startDate: LocalDateTime, endDate: LocalDateTime, agentId: Long): Int {
return queryFactory
.select(member.id.countDistinct())
.from(useCanCalculate)
.innerJoin(useCanCalculate.useCan, useCan)
.innerJoin(member)
.on(member.id.eq(useCanCalculate.recipientCreatorId))
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, useCan.createdAt))
)
.where(
useCan.canUsage.eq(CanUsage.CHANNEL_DONATION)
.and(useCan.isRefund.isFalse)
.and(useCanCalculate.status.eq(UseCanCalculateStatus.RECEIVED))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.fetchOne()
?.toInt()
?: 0
}
fun getChannelDonationByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long
): List<GetAgentChannelDonationSettlementByCreatorQueryData> {
return getChannelDonationByCreatorRows(startDate, endDate, agentId, creatorIds = null)
}
fun getChannelDonationByCreator(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long,
offset: Long,
limit: Long
): List<GetAgentChannelDonationSettlementByCreatorQueryData> {
val creatorIds = queryFactory
.select(member.id)
.from(useCanCalculate)
.innerJoin(useCanCalculate.useCan, useCan)
.innerJoin(member)
.on(member.id.eq(useCanCalculate.recipientCreatorId))
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, useCan.createdAt))
)
.where(
useCan.canUsage.eq(CanUsage.CHANNEL_DONATION)
.and(useCan.isRefund.isFalse)
.and(useCanCalculate.status.eq(UseCanCalculateStatus.RECEIVED))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
)
.groupBy(member.id)
.orderBy(member.id.desc())
.offset(offset)
.limit(limit)
.fetch()
if (creatorIds.isEmpty()) {
return emptyList()
}
return getChannelDonationByCreatorRows(startDate, endDate, agentId, creatorIds)
}
private fun getCalculateLiveByCreatorRows(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long,
creatorIds: List<Long>?
): List<GetAgentCreatorSettlementSummaryQueryData> {
return queryFactory
.select(
QGetAgentCreatorSettlementSummaryQueryData(
member.id,
member.nickname,
agentCreatorRelation.id,
agentSettlementRatio.id,
useCan.id.count(),
useCan.can.add(useCan.rewardCan).sum(),
creatorSettlementRatio.liveSettlementRatio,
agentSettlementRatio.settlementRatio
)
)
.from(useCan)
.innerJoin(useCan.room, liveRoom)
.innerJoin(liveRoom.member, member)
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, useCan.createdAt))
)
.leftJoin(creatorSettlementRatio)
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.leftJoin(agentSettlementRatio)
.on(appliedAgentSettlementRatioAtEventTime(agentId, useCan.createdAt))
.where(
useCan.isRefund.isFalse
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
.and(if (!creatorIds.isNullOrEmpty()) member.id.`in`(creatorIds) else null)
)
.groupBy(
member.id,
member.nickname,
agentCreatorRelation.id,
agentSettlementRatio.id,
creatorSettlementRatio.liveSettlementRatio,
agentSettlementRatio.settlementRatio
)
.orderBy(member.id.desc())
.fetch()
}
private fun getCalculateContentByCreatorRows(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long,
creatorIds: List<Long>?
): List<GetAgentCreatorSettlementSummaryQueryData> {
val contentSettlementRatio = audioContent.settlementRatio.coalesce(creatorSettlementRatio.contentSettlementRatio)
return queryFactory
.select(
QGetAgentCreatorSettlementSummaryQueryData(
member.id,
member.nickname,
agentCreatorRelation.id,
agentSettlementRatio.id,
order.id.count(),
order.can.sum(),
contentSettlementRatio,
agentSettlementRatio.settlementRatio
)
)
.from(order)
.innerJoin(order.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, order.createdAt))
)
.leftJoin(creatorSettlementRatio)
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.leftJoin(agentSettlementRatio)
.on(appliedAgentSettlementRatioAtEventTime(agentId, order.createdAt))
.where(
order.createdAt.goe(startDate)
.and(order.createdAt.loe(endDate))
.and(order.isActive.isTrue)
.and(if (creatorIds != null && creatorIds.isNotEmpty()) member.id.`in`(creatorIds) else null)
)
.groupBy(
member.id,
member.nickname,
agentCreatorRelation.id,
agentSettlementRatio.id,
contentSettlementRatio,
agentSettlementRatio.settlementRatio
)
.orderBy(member.id.desc())
.fetch()
}
private fun getCalculateCommunityByCreatorRows(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long,
creatorIds: List<Long>?
): List<GetAgentCreatorSettlementSummaryQueryData> {
return queryFactory
.select(
QGetAgentCreatorSettlementSummaryQueryData(
member.id,
member.nickname,
agentCreatorRelation.id,
agentSettlementRatio.id,
useCan.id.count(),
useCan.can.add(useCan.rewardCan).sum(),
creatorSettlementRatio.communitySettlementRatio,
agentSettlementRatio.settlementRatio
)
)
.from(useCan)
.innerJoin(useCan.communityPost, creatorCommunity)
.innerJoin(creatorCommunity.member, member)
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, useCan.createdAt))
)
.leftJoin(creatorSettlementRatio)
.on(
member.id.eq(creatorSettlementRatio.member.id)
.and(creatorSettlementRatio.deletedAt.isNull)
)
.leftJoin(agentSettlementRatio)
.on(appliedAgentSettlementRatioAtEventTime(agentId, useCan.createdAt))
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
.and(if (creatorIds != null && creatorIds.isNotEmpty()) member.id.`in`(creatorIds) else null)
)
.groupBy(
member.id,
member.nickname,
agentCreatorRelation.id,
agentSettlementRatio.id,
creatorSettlementRatio.communitySettlementRatio,
agentSettlementRatio.settlementRatio
)
.orderBy(member.id.desc())
.fetch()
}
private fun getCalculateContentDonationByCreatorRows(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long,
creatorIds: List<Long>?
): List<GetAgentCreatorSettlementSummaryQueryData> {
return queryFactory
.select(
QGetAgentCreatorSettlementSummaryQueryData(
member.id,
member.nickname,
agentCreatorRelation.id,
agentSettlementRatio.id,
useCan.id.count(),
useCan.can.add(useCan.rewardCan).sum(),
Expressions.numberTemplate(Int::class.java, "70"),
agentSettlementRatio.settlementRatio
)
)
.from(useCan)
.innerJoin(useCan.audioContent, audioContent)
.innerJoin(audioContent.member, member)
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, useCan.createdAt))
)
.leftJoin(agentSettlementRatio)
.on(appliedAgentSettlementRatioAtEventTime(agentId, useCan.createdAt))
.where(
useCan.isRefund.isFalse
.and(useCan.canUsage.eq(CanUsage.DONATION))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
.and(if (creatorIds != null && creatorIds.isNotEmpty()) member.id.`in`(creatorIds) else null)
)
.groupBy(
member.id,
member.nickname,
agentCreatorRelation.id,
agentSettlementRatio.id,
agentSettlementRatio.settlementRatio
)
.orderBy(member.id.desc())
.fetch()
}
private fun getChannelDonationByCreatorRows(
startDate: LocalDateTime,
endDate: LocalDateTime,
agentId: Long,
creatorIds: List<Long>?
): List<GetAgentChannelDonationSettlementByCreatorQueryData> {
return queryFactory
.select(
QGetAgentChannelDonationSettlementByCreatorQueryData(
member.id,
member.nickname,
agentCreatorRelation.id,
agentSettlementRatio.id,
useCan.id.countDistinct(),
useCanCalculate.can.sum(),
agentSettlementRatio.settlementRatio
)
)
.from(useCanCalculate)
.innerJoin(useCanCalculate.useCan, useCan)
.innerJoin(member)
.on(member.id.eq(useCanCalculate.recipientCreatorId))
.innerJoin(agentCreatorRelation)
.on(
agentCreatorRelation.creator.id.eq(member.id)
.and(assignedToAgentAtEventTime(agentId, useCan.createdAt))
)
.leftJoin(agentSettlementRatio)
.on(appliedAgentSettlementRatioAtEventTime(agentId, useCan.createdAt))
.where(
useCan.canUsage.eq(CanUsage.CHANNEL_DONATION)
.and(useCan.isRefund.isFalse)
.and(useCanCalculate.status.eq(UseCanCalculateStatus.RECEIVED))
.and(useCan.createdAt.goe(startDate))
.and(useCan.createdAt.loe(endDate))
.and(if (creatorIds != null && creatorIds.isNotEmpty()) member.id.`in`(creatorIds) else null)
)
.groupBy(
member.id,
member.nickname,
agentCreatorRelation.id,
agentSettlementRatio.id,
agentSettlementRatio.settlementRatio
)
.orderBy(member.id.desc())
.fetch()
}
private fun assignedToAgentAtEventTime(
agentId: Long,
eventTime: DateTimeExpression<LocalDateTime>
) = agentCreatorRelation.agent.id.eq(agentId)
.and(agentCreatorRelation.assignedAt.loe(eventTime))
.and(agentCreatorRelation.unassignedAt.isNull.or(agentCreatorRelation.unassignedAt.gt(eventTime)))
private fun assignedToAgentAtCurrentTime(
agentId: Long,
currentTime: LocalDateTime
) = agentCreatorRelation.agent.id.eq(agentId)
.and(agentCreatorRelation.assignedAt.loe(currentTime))
.and(agentCreatorRelation.unassignedAt.isNull.or(agentCreatorRelation.unassignedAt.gt(currentTime)))
private fun appliedAgentSettlementRatioAtEventTime(
agentId: Long,
eventTime: DateTimeExpression<LocalDateTime>
) = agentSettlementRatio.member.id.eq(agentId)
.and(agentSettlementRatio.effectiveFrom.loe(eventTime))
.and(agentSettlementRatio.effectiveTo.isNull.or(agentSettlementRatio.effectiveTo.gt(eventTime)))
}

View File

@@ -0,0 +1,219 @@
package kr.co.vividnext.sodalive.partner.agent.calculate
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
import kr.co.vividnext.sodalive.partner.agent.settlement.snapshot.AgentSettlementSnapshotRepository
import kr.co.vividnext.sodalive.partner.agent.settlement.snapshot.AgentSettlementSnapshotType
import kr.co.vividnext.sodalive.partner.agent.settlement.snapshot.toChannelDonationSettlementByCreatorItems
import kr.co.vividnext.sodalive.partner.agent.settlement.snapshot.toSettlementByCreatorItems
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
@Service
class AgentCalculateService(
private val repository: AgentCalculateQueryRepository,
private val snapshotRepository: AgentSettlementSnapshotRepository
) {
@Transactional(readOnly = true)
fun getAssignedCreators(agentId: Long, offset: Long, limit: Long): GetAgentAssignedCreatorResponse {
val currentTime = LocalDateTime.now()
val totalCount = repository.getAssignedCreatorTotalCount(agentId = agentId, currentTime = currentTime)
val items = repository.getAssignedCreators(agentId = agentId, offset = offset, limit = limit, currentTime = currentTime)
return GetAgentAssignedCreatorResponse(totalCount = totalCount, items = items)
}
@Transactional(readOnly = true)
fun getCalculateLiveByCreator(
startDateStr: String,
endDateStr: String,
agentId: Long,
offset: Long,
limit: Long
): GetAgentSettlementByCreatorResponse {
return buildSettlementByCreatorResponse(
startDateStr = startDateStr,
endDateStr = endDateStr,
settlementType = AgentSettlementSnapshotType.LIVE,
agentId = agentId,
offset = offset,
limit = limit,
totalCountLoader = { startDate, endDate ->
repository.getCalculateLiveByCreatorTotalCount(startDate, endDate, agentId)
},
totalRowsLoader = { startDate, endDate ->
repository.getCalculateLiveByCreator(startDate, endDate, agentId)
},
pagedRowsLoader = { startDate, endDate ->
repository.getCalculateLiveByCreator(startDate, endDate, agentId, offset, limit)
}
)
}
@Transactional(readOnly = true)
fun getCalculateContentByCreator(
startDateStr: String,
endDateStr: String,
agentId: Long,
offset: Long,
limit: Long
): GetAgentSettlementByCreatorResponse {
return buildSettlementByCreatorResponse(
startDateStr = startDateStr,
endDateStr = endDateStr,
settlementType = AgentSettlementSnapshotType.CONTENT,
agentId = agentId,
offset = offset,
limit = limit,
totalCountLoader = { startDate, endDate ->
repository.getCalculateContentByCreatorTotalCount(startDate, endDate, agentId)
},
totalRowsLoader = { startDate, endDate ->
repository.getCalculateContentByCreator(startDate, endDate, agentId)
},
pagedRowsLoader = { startDate, endDate ->
repository.getCalculateContentByCreator(startDate, endDate, agentId, offset, limit)
}
)
}
@Transactional(readOnly = true)
fun getCalculateCommunityByCreator(
startDateStr: String,
endDateStr: String,
agentId: Long,
offset: Long,
limit: Long
): GetAgentSettlementByCreatorResponse {
return buildSettlementByCreatorResponse(
startDateStr = startDateStr,
endDateStr = endDateStr,
settlementType = AgentSettlementSnapshotType.COMMUNITY,
agentId = agentId,
offset = offset,
limit = limit,
totalCountLoader = { startDate, endDate ->
repository.getCalculateCommunityByCreatorTotalCount(startDate, endDate, agentId)
},
totalRowsLoader = { startDate, endDate ->
repository.getCalculateCommunityByCreator(startDate, endDate, agentId)
},
pagedRowsLoader = { startDate, endDate ->
repository.getCalculateCommunityByCreator(startDate, endDate, agentId, offset, limit)
}
)
}
@Transactional(readOnly = true)
fun getCalculateContentDonationByCreator(
startDateStr: String,
endDateStr: String,
agentId: Long,
offset: Long,
limit: Long
): GetAgentSettlementByCreatorResponse {
return buildSettlementByCreatorResponse(
startDateStr = startDateStr,
endDateStr = endDateStr,
settlementType = AgentSettlementSnapshotType.CONTENT_DONATION,
agentId = agentId,
offset = offset,
limit = limit,
totalCountLoader = { startDate, endDate ->
repository.getCalculateContentDonationByCreatorTotalCount(startDate, endDate, agentId)
},
totalRowsLoader = { startDate, endDate ->
repository.getCalculateContentDonationByCreator(startDate, endDate, agentId)
},
pagedRowsLoader = { startDate, endDate ->
repository.getCalculateContentDonationByCreator(startDate, endDate, agentId, offset, limit)
}
)
}
@Transactional(readOnly = true)
fun getChannelDonationByCreator(
startDateStr: String,
endDateStr: String,
agentId: Long,
offset: Long,
limit: Long
): GetAgentChannelDonationSettlementByCreatorResponse {
val (startDate, endDate) = toDateRange(startDateStr, endDateStr)
val snapshots = snapshotRepository.findAllByPeriodStartAndPeriodEndAndSettlementTypeAndAgentIdOrderByCreatorIdDesc(
periodStart = startDate,
periodEnd = endDate,
settlementType = AgentSettlementSnapshotType.CHANNEL_DONATION,
agentId = agentId
)
if (snapshots.isNotEmpty()) {
val total = snapshots.toChannelDonationSettlementByCreatorItems().toResponseTotal()
val items = snapshots.drop(offset.toInt()).take(limit.toInt()).toChannelDonationSettlementByCreatorItems()
return GetAgentChannelDonationSettlementByCreatorResponse(
totalCount = snapshots.size,
total = total,
items = items
)
}
val totalCount = repository.getChannelDonationByCreatorTotalCount(startDate, endDate, agentId)
val total = repository.getChannelDonationByCreator(startDate, endDate, agentId)
.toMergedResponseItems()
.toResponseTotal()
val items = repository.getChannelDonationByCreator(startDate, endDate, agentId, offset, limit)
.toMergedResponseItems()
return GetAgentChannelDonationSettlementByCreatorResponse(
totalCount = totalCount,
total = total,
items = items
)
}
private fun buildSettlementByCreatorResponse(
startDateStr: String,
endDateStr: String,
settlementType: AgentSettlementSnapshotType,
agentId: Long,
offset: Long,
limit: Long,
totalCountLoader: (LocalDateTime, LocalDateTime) -> Int,
totalRowsLoader: (LocalDateTime, LocalDateTime) -> List<GetAgentCreatorSettlementSummaryQueryData>,
pagedRowsLoader: (LocalDateTime, LocalDateTime) -> List<GetAgentCreatorSettlementSummaryQueryData>
): GetAgentSettlementByCreatorResponse {
val (startDate, endDate) = toDateRange(startDateStr, endDateStr)
val snapshots = snapshotRepository.findAllByPeriodStartAndPeriodEndAndSettlementTypeAndAgentIdOrderByCreatorIdDesc(
periodStart = startDate,
periodEnd = endDate,
settlementType = settlementType,
agentId = agentId
)
if (snapshots.isNotEmpty()) {
val total = snapshots.toSettlementByCreatorItems().toResponseTotal()
val items = snapshots.drop(offset.toInt()).take(limit.toInt()).toSettlementByCreatorItems()
return GetAgentSettlementByCreatorResponse(
totalCount = snapshots.size,
total = total,
items = items
)
}
val totalCount = totalCountLoader(startDate, endDate)
val total = totalRowsLoader(startDate, endDate)
.toMergedResponseItems()
.toResponseTotal()
val items = pagedRowsLoader(startDate, endDate)
.toMergedResponseItems()
return GetAgentSettlementByCreatorResponse(
totalCount = totalCount,
total = total,
items = items
)
}
private fun toDateRange(startDateStr: String, endDateStr: String): Pair<LocalDateTime, LocalDateTime> {
val startDate = startDateStr.convertLocalDateTime()
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
return startDate to endDate
}
}

View File

@@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.partner.agent.calculate
import com.querydsl.core.annotations.QueryProjection
data class GetAgentAssignedCreatorResponse(
val totalCount: Int,
val items: List<GetAgentAssignedCreatorItem>
)
data class GetAgentAssignedCreatorItem @QueryProjection constructor(
val creatorId: Long,
val creatorNickname: String
)

View File

@@ -0,0 +1,102 @@
package kr.co.vividnext.sodalive.partner.agent.calculate
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.calculate.channelDonation.ChannelDonationSettlementCalculator
import java.math.BigDecimal
import java.math.RoundingMode
data class GetAgentChannelDonationSettlementByCreatorResponse(
val totalCount: Int,
val total: GetAgentChannelDonationSettlementTotal,
val items: List<GetAgentChannelDonationSettlementByCreatorItem>
)
data class GetAgentChannelDonationSettlementTotal(
val count: Int,
val totalCan: Int,
val krw: Int,
val fee: Int,
val settlementAmount: Int,
val withholdingTax: Int,
val depositAmount: Int,
val agentSettlementAmount: Int
)
data class GetAgentChannelDonationSettlementByCreatorItem(
val creatorId: Long,
val creatorNickname: String,
val count: Int,
val totalCan: Int,
val krw: Int,
val fee: Int,
val settlementAmount: Int,
val withholdingTax: Int,
val depositAmount: Int,
val agentSettlementAmount: Int
)
data class GetAgentChannelDonationSettlementByCreatorQueryData @QueryProjection constructor(
val creatorId: Long,
val creatorNickname: String,
val assignmentId: Long? = null,
val agentSettlementRatioId: Long? = null,
val count: Long,
val totalCan: Int,
val agentSettlementRatio: Int? = null
) {
fun toResponseItem(): GetAgentChannelDonationSettlementByCreatorItem {
val amount = ChannelDonationSettlementCalculator.calculate(totalCan)
return GetAgentChannelDonationSettlementByCreatorItem(
creatorId = creatorId,
creatorNickname = creatorNickname,
count = count.toInt(),
totalCan = totalCan,
krw = amount.krw,
fee = amount.fee,
settlementAmount = amount.settlementAmount,
withholdingTax = amount.withholdingTax,
depositAmount = amount.depositAmount,
agentSettlementAmount = BigDecimal(amount.settlementAmount)
.multiply(BigDecimal(agentSettlementRatio ?: DEFAULT_AGENT_SETTLEMENT_RATIO).divide(BigDecimal("100")))
.setScale(0, RoundingMode.HALF_UP)
.toInt()
)
}
companion object {
private const val DEFAULT_AGENT_SETTLEMENT_RATIO = 10
}
}
fun List<GetAgentChannelDonationSettlementByCreatorQueryData>.toMergedResponseItems():
List<GetAgentChannelDonationSettlementByCreatorItem> {
return map { it.toResponseItem() }
.groupBy { it.creatorId }
.map { (_, items) ->
items.reduce { acc, item ->
acc.copy(
count = acc.count + item.count,
totalCan = acc.totalCan + item.totalCan,
krw = acc.krw + item.krw,
fee = acc.fee + item.fee,
settlementAmount = acc.settlementAmount + item.settlementAmount,
withholdingTax = acc.withholdingTax + item.withholdingTax,
depositAmount = acc.depositAmount + item.depositAmount,
agentSettlementAmount = acc.agentSettlementAmount + item.agentSettlementAmount
)
}
}
}
fun List<GetAgentChannelDonationSettlementByCreatorItem>.toResponseTotal(): GetAgentChannelDonationSettlementTotal {
return GetAgentChannelDonationSettlementTotal(
count = sumOf { it.count },
totalCan = sumOf { it.totalCan },
krw = sumOf { it.krw },
fee = sumOf { it.fee },
settlementAmount = sumOf { it.settlementAmount },
withholdingTax = sumOf { it.withholdingTax },
depositAmount = sumOf { it.depositAmount },
agentSettlementAmount = sumOf { it.agentSettlementAmount }
)
}

View File

@@ -0,0 +1,77 @@
package kr.co.vividnext.sodalive.partner.agent.calculate
import com.querydsl.core.annotations.QueryProjection
import java.math.BigDecimal
import java.math.RoundingMode
data class GetAgentCreatorSettlementSummaryQueryData @QueryProjection constructor(
val creatorId: Long,
val creatorNickname: String,
val assignmentId: Long? = null,
val agentSettlementRatioId: Long? = null,
val count: Long,
val totalCan: Int,
val settlementRatio: Int?,
val agentSettlementRatio: Int? = null
) {
fun toResponseItem(): GetAgentSettlementByCreatorItem {
val totalKrw = BigDecimal(totalCan).multiply(KRW_PER_CAN)
val fee = totalKrw.multiply(PAYMENT_FEE_RATE)
val settlementAmount = totalKrw.subtract(fee)
.multiply(BigDecimal(settlementRatio ?: DEFAULT_SETTLEMENT_RATIO).divide(PERCENT_DIVISOR))
val tax = settlementAmount.multiply(TAX_RATE)
val depositAmount = settlementAmount.subtract(tax)
val roundedSettlementAmount = settlementAmount.setScale(0, RoundingMode.HALF_UP).toInt()
return GetAgentSettlementByCreatorItem(
creatorId = creatorId,
creatorNickname = creatorNickname,
count = count.toInt(),
totalCan = totalCan,
krw = totalKrw.toInt(),
fee = fee.setScale(0, RoundingMode.HALF_UP).toInt(),
settlementAmount = roundedSettlementAmount,
tax = tax.setScale(0, RoundingMode.HALF_UP).toInt(),
depositAmount = depositAmount.setScale(0, RoundingMode.HALF_UP).toInt(),
agentSettlementAmount = calculateAgentSettlementAmount(
settlementAmount = roundedSettlementAmount,
agentSettlementRatio = agentSettlementRatio ?: DEFAULT_AGENT_SETTLEMENT_RATIO
)
)
}
companion object {
private val KRW_PER_CAN = BigDecimal("100")
private val PAYMENT_FEE_RATE = BigDecimal("0.066")
private val TAX_RATE = BigDecimal("0.033")
private val PERCENT_DIVISOR = BigDecimal("100")
private const val DEFAULT_SETTLEMENT_RATIO = 70
private const val DEFAULT_AGENT_SETTLEMENT_RATIO = 10
private fun calculateAgentSettlementAmount(settlementAmount: Int, agentSettlementRatio: Int): Int {
return BigDecimal(settlementAmount)
.multiply(BigDecimal(agentSettlementRatio).divide(PERCENT_DIVISOR))
.setScale(0, RoundingMode.HALF_UP)
.toInt()
}
}
}
fun List<GetAgentCreatorSettlementSummaryQueryData>.toMergedResponseItems(): List<GetAgentSettlementByCreatorItem> {
return map { it.toResponseItem() }
.groupBy { it.creatorId }
.map { (_, items) ->
items.reduce { acc, item ->
acc.copy(
count = acc.count + item.count,
totalCan = acc.totalCan + item.totalCan,
krw = acc.krw + item.krw,
fee = acc.fee + item.fee,
settlementAmount = acc.settlementAmount + item.settlementAmount,
tax = acc.tax + item.tax,
depositAmount = acc.depositAmount + item.depositAmount,
agentSettlementAmount = acc.agentSettlementAmount + item.agentSettlementAmount
)
}
}
}

View File

@@ -0,0 +1,44 @@
package kr.co.vividnext.sodalive.partner.agent.calculate
data class GetAgentSettlementByCreatorResponse(
val totalCount: Int,
val total: GetAgentSettlementByCreatorTotal,
val items: List<GetAgentSettlementByCreatorItem>
)
data class GetAgentSettlementByCreatorTotal(
val count: Int,
val totalCan: Int,
val krw: Int,
val fee: Int,
val settlementAmount: Int,
val tax: Int,
val depositAmount: Int,
val agentSettlementAmount: Int
)
data class GetAgentSettlementByCreatorItem(
val creatorId: Long,
val creatorNickname: String,
val count: Int,
val totalCan: Int,
val krw: Int,
val fee: Int,
val settlementAmount: Int,
val tax: Int,
val depositAmount: Int,
val agentSettlementAmount: Int
)
fun List<GetAgentSettlementByCreatorItem>.toResponseTotal(): GetAgentSettlementByCreatorTotal {
return GetAgentSettlementByCreatorTotal(
count = sumOf { it.count },
totalCan = sumOf { it.totalCan },
krw = sumOf { it.krw },
fee = sumOf { it.fee },
settlementAmount = sumOf { it.settlementAmount },
tax = sumOf { it.tax },
depositAmount = sumOf { it.depositAmount },
agentSettlementAmount = sumOf { it.agentSettlementAmount }
)
}

View File

@@ -0,0 +1,326 @@
package kr.co.vividnext.sodalive.partner.agent.calculate
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import org.junit.jupiter.api.Assertions.assertEquals
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
import org.mockito.Mockito
import org.springframework.data.domain.PageRequest
class AgentCalculateControllerTest {
private lateinit var service: AgentCalculateService
private lateinit var controller: AgentCalculateController
@BeforeEach
fun setup() {
service = Mockito.mock(AgentCalculateService::class.java)
controller = AgentCalculateController(service)
}
@Test
@DisplayName("인증 사용자 정보가 없으면 소속 크리에이터 목록 조회는 예외를 던진다")
fun shouldThrowWhenMemberIsNullForAssignedCreators() {
val exception = assertThrows(SodaException::class.java) {
controller.getAssignedCreators(
member = null,
pageable = PageRequest.of(0, 10)
)
}
assertEquals("common.error.bad_credentials", exception.messageKey)
}
@Test
@DisplayName("에이전트 컨트롤러는 소속 크리에이터 목록 조회 파라미터를 서비스로 전달한다")
fun shouldForwardAssignedCreatorsRequestToService() {
val member = createAgentMember(7L)
val response = GetAgentAssignedCreatorResponse(
totalCount = 1,
items = listOf(
GetAgentAssignedCreatorItem(
creatorId = 21L,
creatorNickname = "creator-a"
)
)
)
Mockito.`when`(service.getAssignedCreators(agentId = 7L, offset = 10L, limit = 5L)).thenReturn(response)
val apiResponse = controller.getAssignedCreators(
member = member,
pageable = PageRequest.of(2, 5)
)
assertEquals(true, apiResponse.success)
assertEquals(1, apiResponse.data!!.totalCount)
assertEquals(21L, apiResponse.data!!.items[0].creatorId)
Mockito.verify(service).getAssignedCreators(agentId = 7L, offset = 10L, limit = 5L)
}
@Test
@DisplayName("에이전트 컨트롤러는 라이브 요약 조회 파라미터를 서비스로 전달한다")
fun shouldForwardLiveSummaryRequestToService() {
val member = createAgentMember(7L)
val response = GetAgentSettlementByCreatorResponse(
totalCount = 1,
total = GetAgentSettlementByCreatorTotal(
count = 2,
totalCan = 40,
krw = 4000,
fee = 264,
settlementAmount = 2615,
tax = 86,
depositAmount = 2529,
agentSettlementAmount = 262
),
items = listOf(
GetAgentSettlementByCreatorItem(
creatorId = 21L,
creatorNickname = "creator-a",
count = 2,
totalCan = 40,
krw = 4000,
fee = 264,
settlementAmount = 2615,
tax = 86,
depositAmount = 2529,
agentSettlementAmount = 262
)
)
)
Mockito.`when`(
service.getCalculateLiveByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 20L
)
).thenReturn(response)
val apiResponse = controller.getCalculateLiveByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
member = member,
pageable = PageRequest.of(0, 20)
)
assertEquals(true, apiResponse.success)
assertEquals(262, apiResponse.data!!.items[0].agentSettlementAmount)
Mockito.verify(service).getCalculateLiveByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 20L
)
}
@Test
@DisplayName("에이전트 컨트롤러는 콘텐츠 요약 조회 파라미터를 서비스로 전달한다")
fun shouldForwardContentSummaryRequestToService() {
val member = createAgentMember(7L)
val response = createGenericSummaryResponse(agentSettlementAmount = 374)
Mockito.`when`(
service.getCalculateContentByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 20L,
limit = 20L
)
).thenReturn(response)
val apiResponse = controller.getCalculateContentByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
member = member,
pageable = PageRequest.of(1, 20)
)
assertEquals(true, apiResponse.success)
assertEquals(374, apiResponse.data!!.items[0].agentSettlementAmount)
Mockito.verify(service).getCalculateContentByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 20L,
limit = 20L
)
}
@Test
@DisplayName("에이전트 컨트롤러는 커뮤니티 요약 조회 파라미터를 서비스로 전달한다")
fun shouldForwardCommunitySummaryRequestToService() {
val member = createAgentMember(7L)
val response = createGenericSummaryResponse(agentSettlementAmount = 168)
Mockito.`when`(
service.getCalculateCommunityByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 10L
)
).thenReturn(response)
val apiResponse = controller.getCalculateCommunityByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
member = member,
pageable = PageRequest.of(0, 10)
)
assertEquals(true, apiResponse.success)
assertEquals(168, apiResponse.data!!.items[0].agentSettlementAmount)
Mockito.verify(service).getCalculateCommunityByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 10L
)
}
@Test
@DisplayName("에이전트 컨트롤러는 채널후원 요약 조회 파라미터를 서비스로 전달한다")
fun shouldForwardChannelDonationSummaryRequestToService() {
val member = createAgentMember(7L)
val response = GetAgentChannelDonationSettlementByCreatorResponse(
totalCount = 1,
total = GetAgentChannelDonationSettlementTotal(
count = 1,
totalCan = 50,
krw = 5000,
fee = 330,
settlementAmount = 3970,
withholdingTax = 131,
depositAmount = 3839,
agentSettlementAmount = 397
),
items = listOf(
GetAgentChannelDonationSettlementByCreatorItem(
creatorId = 21L,
creatorNickname = "creator-a",
count = 1,
totalCan = 50,
krw = 5000,
fee = 330,
settlementAmount = 3970,
withholdingTax = 131,
depositAmount = 3839,
agentSettlementAmount = 397
)
)
)
Mockito.`when`(
service.getChannelDonationByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 10L
)
).thenReturn(response)
val apiResponse = controller.getChannelDonationByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
member = member,
pageable = PageRequest.of(0, 10)
)
assertEquals(true, apiResponse.success)
assertEquals(397, apiResponse.data!!.items[0].agentSettlementAmount)
Mockito.verify(service).getChannelDonationByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 10L
)
}
@Test
@DisplayName("에이전트 컨트롤러는 콘텐츠후원 요약 조회 파라미터를 서비스로 전달한다")
fun shouldForwardContentDonationSummaryRequestToService() {
val member = createAgentMember(7L)
val response = createGenericSummaryResponse(agentSettlementAmount = 131)
Mockito.`when`(
service.getCalculateContentDonationByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 10L
)
).thenReturn(response)
val apiResponse = controller.getCalculateContentDonationByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
member = member,
pageable = PageRequest.of(0, 10)
)
assertEquals(true, apiResponse.success)
assertEquals(131, apiResponse.data!!.items[0].agentSettlementAmount)
Mockito.verify(service).getCalculateContentDonationByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 10L
)
}
private fun createAgentMember(id: Long): Member {
val member = Member(
email = "agent@test.com",
password = "password",
nickname = "agent",
role = MemberRole.AGENT
)
member.id = id
return member
}
private fun createGenericSummaryResponse(agentSettlementAmount: Int): GetAgentSettlementByCreatorResponse {
return GetAgentSettlementByCreatorResponse(
totalCount = 1,
total = GetAgentSettlementByCreatorTotal(
count = 1,
totalCan = 50,
krw = 5000,
fee = 330,
settlementAmount = 3736,
tax = 123,
depositAmount = 3613,
agentSettlementAmount = agentSettlementAmount
),
items = listOf(
GetAgentSettlementByCreatorItem(
creatorId = 21L,
creatorNickname = "creator-a",
count = 1,
totalCan = 50,
krw = 5000,
fee = 330,
settlementAmount = 3736,
tax = 123,
depositAmount = 3613,
agentSettlementAmount = agentSettlementAmount
)
)
)
}
}

View File

@@ -0,0 +1,777 @@
package kr.co.vividnext.sodalive.partner.agent.calculate
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.admin.calculate.ratio.CreatorSettlementRatio
import kr.co.vividnext.sodalive.admin.calculate.ratio.CreatorSettlementRatioRepository
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.UseCan
import kr.co.vividnext.sodalive.can.use.UseCanCalculate
import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
import kr.co.vividnext.sodalive.can.use.UseCanRepository
import kr.co.vividnext.sodalive.configs.QueryDslConfig
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.AudioContentRepository
import kr.co.vividnext.sodalive.content.order.Order
import kr.co.vividnext.sodalive.content.order.OrderRepository
import kr.co.vividnext.sodalive.content.order.OrderType
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityRepository
import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.live.room.LiveRoomRepository
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.partner.agent.assignment.AgentCreatorRelation
import kr.co.vividnext.sodalive.partner.agent.assignment.AgentCreatorRelationRepository
import kr.co.vividnext.sodalive.partner.agent.ratio.AgentSettlementRatio
import kr.co.vividnext.sodalive.partner.agent.ratio.AgentSettlementRatioRepository
import kr.co.vividnext.sodalive.partner.agent.settlement.snapshot.AgentSettlementSnapshotRepository
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.context.annotation.Import
import java.time.LocalDateTime
import javax.persistence.EntityManager
@DataJpaTest(properties = ["spring.jpa.database-platform=kr.co.vividnext.sodalive.support.H2MySqlFunctionDialect"])
@Import(QueryDslConfig::class)
class AgentCalculateQueryRepositoryTest @Autowired constructor(
private val queryFactory: JPAQueryFactory,
private val memberRepository: MemberRepository,
private val relationRepository: AgentCreatorRelationRepository,
private val agentSettlementRatioRepository: AgentSettlementRatioRepository,
private val snapshotRepository: AgentSettlementSnapshotRepository,
private val creatorSettlementRatioRepository: CreatorSettlementRatioRepository,
private val audioContentRepository: AudioContentRepository,
private val orderRepository: OrderRepository,
private val liveRoomRepository: LiveRoomRepository,
private val creatorCommunityRepository: CreatorCommunityRepository,
private val useCanRepository: UseCanRepository,
private val useCanCalculateRepository: UseCanCalculateRepository,
private val entityManager: EntityManager
) {
private lateinit var repository: AgentCalculateQueryRepository
private lateinit var service: AgentCalculateService
@BeforeEach
fun setup() {
registerMysqlDateFunctions()
repository = AgentCalculateQueryRepository(queryFactory)
service = AgentCalculateService(repository, snapshotRepository)
}
@Test
@DisplayName("소속 크리에이터 목록 조회는 현재 에이전트에 연결된 크리에이터만 반환한다")
fun shouldGetAssignedCreatorsOnlyForCurrentAgent() {
val agent = saveMember("agent-a", MemberRole.AGENT)
val otherAgent = saveMember("agent-b", MemberRole.AGENT)
val creatorA = saveMember("creator-a", MemberRole.CREATOR)
val creatorB = saveMember("creator-b", MemberRole.CREATOR)
val creatorC = saveMember("creator-c", MemberRole.CREATOR)
val currentTime = LocalDateTime.of(2026, 2, 20, 12, 0, 0)
saveRelation(agent, creatorA)
saveRelation(agent, creatorB)
saveRelation(otherAgent, creatorC)
val totalCount = repository.getAssignedCreatorTotalCount(agent.id!!, currentTime)
val items = repository.getAssignedCreators(agent.id!!, offset = 0, limit = 10, currentTime = currentTime)
assertEquals(2, totalCount)
assertEquals(2, items.size)
assertEquals(listOf(creatorB.id, creatorA.id), items.map { it.creatorId })
}
@Test
@DisplayName("소속 크리에이터 목록 조회는 현재 시각 활성 구간의 크리에이터만 반환한다")
fun shouldGetAssignedCreatorsOnlyWithinCurrentAssignmentWindow() {
val agent = saveMember("agent-window", MemberRole.AGENT)
val currentCreator = saveMember("creator-current", MemberRole.CREATOR)
val futureCreator = saveMember("creator-future", MemberRole.CREATOR)
val endingFutureCreator = saveMember("creator-ending-future", MemberRole.CREATOR)
val endedCreator = saveMember("creator-ended", MemberRole.CREATOR)
val currentTime = LocalDateTime.now()
saveRelation(agent, currentCreator, assignedAt = currentTime.minusDays(2), unassignedAt = null)
saveRelation(agent, futureCreator, assignedAt = currentTime.plusDays(1), unassignedAt = null)
saveRelation(agent, endingFutureCreator, assignedAt = currentTime.minusDays(2), unassignedAt = currentTime.plusDays(1))
saveRelation(agent, endedCreator, assignedAt = currentTime.minusDays(3), unassignedAt = currentTime.minusHours(1))
val totalCount = repository.getAssignedCreatorTotalCount(agent.id!!, currentTime)
val items = repository.getAssignedCreators(agent.id!!, offset = 0, limit = 10, currentTime = currentTime)
assertEquals(2, totalCount)
assertEquals(listOf(endingFutureCreator.id, currentCreator.id), items.map { it.creatorId })
}
@Test
@DisplayName("라이브 크리에이터별 조회는 소속된 크리에이터만 집계한다")
fun shouldGetLiveSummaryRowsOnlyForAssignedCreators() {
val agent = saveMember("agent-live", MemberRole.AGENT)
val creator = saveMember("creator-live", MemberRole.CREATOR)
val otherCreator = saveMember("creator-other-live", MemberRole.CREATOR)
val sender = saveMember("sender-live", MemberRole.USER)
saveRelation(agent, creator)
saveCreatorSettlementRatio(creator, live = 70, content = 70, community = 70)
saveCreatorSettlementRatio(otherCreator, live = 80, content = 80, community = 80)
val room = saveLiveRoom(creator)
val otherRoom = saveLiveRoom(otherCreator)
saveLiveUseCan(sender, room, 10, LocalDateTime.of(2026, 2, 20, 10, 0, 0))
saveLiveUseCan(sender, room, 30, LocalDateTime.of(2026, 2, 20, 11, 0, 0))
saveLiveUseCan(sender, otherRoom, 50, LocalDateTime.of(2026, 2, 20, 12, 0, 0))
val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0)
val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59)
val totalCount = repository.getCalculateLiveByCreatorTotalCount(startDate, endDate, agent.id!!)
val rows = repository.getCalculateLiveByCreator(startDate, endDate, agent.id!!)
assertEquals(1, totalCount)
assertEquals(1, rows.size)
assertEquals(creator.id, rows[0].creatorId)
assertEquals(2L, rows[0].count)
assertEquals(40, rows[0].totalCan)
assertEquals(70, rows[0].settlementRatio)
}
@Test
@DisplayName("콘텐츠 크리에이터별 조회는 콘텐츠 개별 정산 비율과 기본 정산 비율을 모두 반영한다")
fun shouldGetContentSummaryRowsGroupedByEffectiveSettlementRatio() {
val agent = saveMember("agent-content", MemberRole.AGENT)
val creator = saveMember("creator-content", MemberRole.CREATOR)
val otherCreator = saveMember("creator-other-content", MemberRole.CREATOR)
val buyer = saveMember("buyer-content", MemberRole.USER)
saveRelation(agent, creator)
saveCreatorSettlementRatio(creator, live = 70, content = 60, community = 70)
saveCreatorSettlementRatio(otherCreator, live = 70, content = 90, community = 70)
val paidContent = saveAudioContent(creator, "content-a", price = 50, settlementRatio = 80)
val fallbackContent = saveAudioContent(creator, "content-b", price = 30, settlementRatio = null)
val otherContent = saveAudioContent(otherCreator, "content-c", price = 90, 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))
saveOrder(buyer, otherCreator, otherContent, LocalDateTime.of(2026, 2, 20, 11, 0, 0))
val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0)
val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59)
val totalCount = repository.getCalculateContentByCreatorTotalCount(startDate, endDate, agent.id!!)
val rows = repository.getCalculateContentByCreator(startDate, endDate, agent.id!!)
assertEquals(1, totalCount)
assertEquals(2, rows.size)
assertEquals(listOf(80, 60), rows.mapNotNull { it.settlementRatio }.sortedDescending())
assertEquals(listOf(50, 30), rows.map { it.totalCan }.sortedDescending())
assertEquals(listOf(creator.id!!, creator.id!!), rows.map { it.creatorId })
}
@Test
@DisplayName("커뮤니티 크리에이터별 조회는 소속된 크리에이터만 집계한다")
fun shouldGetCommunitySummaryRowsOnlyForAssignedCreators() {
val agent = saveMember("agent-community", MemberRole.AGENT)
val creator = saveMember("creator-community", MemberRole.CREATOR)
val otherCreator = saveMember("creator-other-community", MemberRole.CREATOR)
val buyer = saveMember("buyer-community", MemberRole.USER)
saveRelation(agent, creator)
saveCreatorSettlementRatio(creator, live = 70, content = 70, community = 60)
saveCreatorSettlementRatio(otherCreator, live = 70, content = 70, community = 80)
val communityPost = saveCommunityPost(creator, 10)
val otherPost = saveCommunityPost(otherCreator, 20)
saveCommunityUseCan(buyer, communityPost, 20, LocalDateTime.of(2026, 2, 20, 9, 0, 0))
saveCommunityUseCan(buyer, communityPost, 10, LocalDateTime.of(2026, 2, 20, 10, 0, 0))
saveCommunityUseCan(buyer, otherPost, 50, LocalDateTime.of(2026, 2, 20, 11, 0, 0))
val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0)
val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59)
val totalCount = repository.getCalculateCommunityByCreatorTotalCount(startDate, endDate, agent.id!!)
val rows = repository.getCalculateCommunityByCreator(startDate, endDate, agent.id!!)
assertEquals(1, totalCount)
assertEquals(1, rows.size)
assertEquals(creator.id, rows[0].creatorId)
assertEquals(2L, rows[0].count)
assertEquals(30, rows[0].totalCan)
assertEquals(60, rows[0].settlementRatio)
}
@Test
@DisplayName("콘텐츠후원 크리에이터별 조회는 소속된 크리에이터만 집계한다")
fun shouldGetContentDonationSummaryRowsOnlyForAssignedCreators() {
val agent = saveMember("agent-content-donation", MemberRole.AGENT)
val creator = saveMember("creator-content-donation", MemberRole.CREATOR)
val otherCreator = saveMember("creator-other-content-donation", MemberRole.CREATOR)
val buyer = saveMember("buyer-content-donation", MemberRole.USER)
saveRelation(agent, creator)
val content = saveAudioContent(creator, "content-donation-a", price = 0, settlementRatio = null)
val otherContent = saveAudioContent(otherCreator, "content-donation-b", price = 0, settlementRatio = null)
saveContentDonationUseCan(buyer, content, 7, LocalDateTime.of(2026, 2, 20, 9, 0, 0))
saveContentDonationUseCan(buyer, content, 13, LocalDateTime.of(2026, 2, 20, 10, 0, 0))
saveContentDonationUseCan(buyer, otherContent, 30, LocalDateTime.of(2026, 2, 20, 11, 0, 0))
val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0)
val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59)
val totalCount = repository.getCalculateContentDonationByCreatorTotalCount(startDate, endDate, agent.id!!)
val rows = repository.getCalculateContentDonationByCreator(startDate, endDate, agent.id!!)
assertEquals(1, totalCount)
assertEquals(1, rows.size)
assertEquals(creator.id, rows[0].creatorId)
assertEquals(2L, rows[0].count)
assertEquals(20, rows[0].totalCan)
assertEquals(70, rows[0].settlementRatio)
}
@Test
@DisplayName("채널후원 크리에이터별 조회는 분할 정산 레코드가 있어도 후원 단위로 집계한다")
fun shouldCountDistinctUseCanForChannelDonationByCreator() {
val agent = saveMember("agent-channel", MemberRole.AGENT)
val creator = saveMember("creator-channel", MemberRole.CREATOR)
val otherCreator = saveMember("creator-other-channel", MemberRole.CREATOR)
val sender = saveMember("sender-channel", MemberRole.USER)
saveRelation(agent, creator)
val useCan = saveChannelDonationUseCan(sender, 50, LocalDateTime.of(2026, 2, 20, 9, 0, 0))
saveUseCanCalculate(useCan, creator.id!!, 20, PaymentGateway.PG)
saveUseCanCalculate(useCan, creator.id!!, 30, PaymentGateway.GOOGLE_IAP)
val otherUseCan = saveChannelDonationUseCan(sender, 40, LocalDateTime.of(2026, 2, 20, 10, 0, 0))
saveUseCanCalculate(otherUseCan, otherCreator.id!!, 40, PaymentGateway.PG)
val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0)
val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59)
val totalCount = repository.getChannelDonationByCreatorTotalCount(startDate, endDate, agent.id!!)
val rows = repository.getChannelDonationByCreator(startDate, endDate, agent.id!!)
assertEquals(1, totalCount)
assertEquals(1, rows.size)
assertEquals(creator.id, rows[0].creatorId)
assertEquals(1L, rows[0].count)
assertEquals(50, rows[0].totalCan)
}
@Test
@DisplayName("페이지 대상 creator가 없으면 모든 카테고리 조회는 빈 rows를 반환한다")
fun shouldReturnEmptyRowsWhenPagedCreatorSelectionIsEmptyAcrossAllCategories() {
val agent = saveMember("agent-empty-page", MemberRole.AGENT)
val creator = saveMember("creator-empty-page", MemberRole.CREATOR)
val buyer = saveMember("buyer-empty-page", MemberRole.USER)
saveRelation(agent, creator)
saveAgentSettlementRatio(agent, settlementRatio = 10, effectiveFrom = LocalDateTime.of(2026, 2, 1, 0, 0, 0))
saveCreatorSettlementRatio(creator, live = 70, content = 70, community = 70)
val room = saveLiveRoom(creator)
saveLiveUseCan(buyer, room, 10, LocalDateTime.of(2026, 2, 20, 9, 0, 0))
val content = saveAudioContent(creator, "empty-page-content", price = 10, settlementRatio = null)
saveOrder(buyer, creator, content, LocalDateTime.of(2026, 2, 20, 10, 0, 0))
val communityPost = saveCommunityPost(creator, 10)
saveCommunityUseCan(buyer, communityPost, 10, LocalDateTime.of(2026, 2, 20, 11, 0, 0))
saveContentDonationUseCan(buyer, content, 10, LocalDateTime.of(2026, 2, 20, 12, 0, 0))
val channelDonation = saveChannelDonationUseCan(buyer, 10, LocalDateTime.of(2026, 2, 20, 13, 0, 0))
saveUseCanCalculate(channelDonation, creator.id!!, 10, PaymentGateway.PG)
val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0)
val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59)
assertEquals(1, repository.getCalculateLiveByCreatorTotalCount(startDate, endDate, agent.id!!))
assertEquals(1, repository.getCalculateContentByCreatorTotalCount(startDate, endDate, agent.id!!))
assertEquals(1, repository.getCalculateCommunityByCreatorTotalCount(startDate, endDate, agent.id!!))
assertEquals(1, repository.getCalculateContentDonationByCreatorTotalCount(startDate, endDate, agent.id!!))
assertEquals(1, repository.getChannelDonationByCreatorTotalCount(startDate, endDate, agent.id!!))
assertEquals(
0,
repository.getCalculateLiveByCreator(startDate, endDate, agent.id!!, offset = 1, limit = 10).size
)
assertEquals(
0,
repository.getCalculateContentByCreator(startDate, endDate, agent.id!!, offset = 1, limit = 10).size
)
assertEquals(
0,
repository.getCalculateCommunityByCreator(startDate, endDate, agent.id!!, offset = 1, limit = 10).size
)
assertEquals(
0,
repository.getCalculateContentDonationByCreator(startDate, endDate, agent.id!!, offset = 1, limit = 10).size
)
assertEquals(
0,
repository.getChannelDonationByCreator(startDate, endDate, agent.id!!, offset = 1, limit = 10).size
)
}
@Test
@DisplayName("정산 조회는 기간 중 소속 변경이 있어도 거래 시점 기준 소속 에이전트에게만 반영한다")
fun shouldResolveAssignmentAtEventTimeAcrossAllSettlementCategories() {
val firstAgent = saveMember("agent-assignment-a", MemberRole.AGENT)
val secondAgent = saveMember("agent-assignment-b", MemberRole.AGENT)
val creator = saveMember("creator-assignment", MemberRole.CREATOR)
val buyer = saveMember("buyer-assignment", MemberRole.USER)
saveAgentSettlementRatio(firstAgent, settlementRatio = 10, effectiveFrom = LocalDateTime.of(2026, 2, 1, 0, 0, 0))
saveAgentSettlementRatio(secondAgent, settlementRatio = 10, effectiveFrom = LocalDateTime.of(2026, 2, 1, 0, 0, 0))
saveRelation(
agent = firstAgent,
creator = creator,
assignedAt = LocalDateTime.of(2026, 2, 1, 0, 0, 0),
unassignedAt = LocalDateTime.of(2026, 2, 20, 12, 0, 0)
)
saveRelation(
agent = secondAgent,
creator = creator,
assignedAt = LocalDateTime.of(2026, 2, 20, 12, 0, 0),
unassignedAt = null
)
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, "assignment-content-before", price = 10, settlementRatio = null)
val contentAfter = saveAudioContent(creator, "assignment-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 channelBefore = saveChannelDonationUseCan(buyer, 10, LocalDateTime.of(2026, 2, 20, 10, 0, 0))
saveUseCanCalculate(channelBefore, creator.id!!, 10, PaymentGateway.PG)
val channelAfter = saveChannelDonationUseCan(buyer, 20, LocalDateTime.of(2026, 2, 20, 14, 0, 0))
saveUseCanCalculate(channelAfter, creator.id!!, 20, PaymentGateway.PG)
val savedRelations = relationRepository.findAllByCreatorIdOrderByAssignedAtAsc(creator.id!!)
val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0)
val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59)
val firstAgentLiveRows = repository.getCalculateLiveByCreator(startDate, endDate, firstAgent.id!!)
val secondAgentLiveRows = repository.getCalculateLiveByCreator(startDate, endDate, secondAgent.id!!)
assertEquals(listOf(savedRelations[0].id), firstAgentLiveRows.map { it.assignmentId })
assertEquals(listOf(savedRelations[1].id), secondAgentLiveRows.map { it.assignmentId })
val firstAgentLive = service.getCalculateLiveByCreator("2026-02-20", "2026-02-20", firstAgent.id!!, 0, 10)
val secondAgentLive = service.getCalculateLiveByCreator("2026-02-20", "2026-02-20", secondAgent.id!!, 0, 10)
assertGenericSettlementResponse(firstAgentLive, expectedCount = 1, expectedTotalCan = 10)
assertGenericSettlementResponse(secondAgentLive, expectedCount = 1, expectedTotalCan = 20)
val firstAgentContent = service.getCalculateContentByCreator("2026-02-20", "2026-02-20", firstAgent.id!!, 0, 10)
val secondAgentContent = service.getCalculateContentByCreator("2026-02-20", "2026-02-20", secondAgent.id!!, 0, 10)
assertGenericSettlementResponse(firstAgentContent, expectedCount = 1, expectedTotalCan = 10)
assertGenericSettlementResponse(secondAgentContent, expectedCount = 1, expectedTotalCan = 20)
val firstAgentCommunity = service.getCalculateCommunityByCreator("2026-02-20", "2026-02-20", firstAgent.id!!, 0, 10)
val secondAgentCommunity = service.getCalculateCommunityByCreator("2026-02-20", "2026-02-20", secondAgent.id!!, 0, 10)
assertGenericSettlementResponse(firstAgentCommunity, expectedCount = 1, expectedTotalCan = 10)
assertGenericSettlementResponse(secondAgentCommunity, expectedCount = 1, expectedTotalCan = 20)
val firstAgentContentDonation = service.getCalculateContentDonationByCreator(
"2026-02-20",
"2026-02-20",
firstAgent.id!!,
0,
10
)
val secondAgentContentDonation = service.getCalculateContentDonationByCreator(
"2026-02-20",
"2026-02-20",
secondAgent.id!!,
0,
10
)
assertGenericSettlementResponse(firstAgentContentDonation, expectedCount = 1, expectedTotalCan = 10)
assertGenericSettlementResponse(secondAgentContentDonation, expectedCount = 1, expectedTotalCan = 20)
val firstAgentChannelDonation = service.getChannelDonationByCreator("2026-02-20", "2026-02-20", firstAgent.id!!, 0, 10)
val secondAgentChannelDonation = service.getChannelDonationByCreator("2026-02-20", "2026-02-20", secondAgent.id!!, 0, 10)
assertChannelDonationSettlementResponse(firstAgentChannelDonation, expectedCount = 1, expectedTotalCan = 10)
assertChannelDonationSettlementResponse(secondAgentChannelDonation, expectedCount = 1, expectedTotalCan = 20)
}
@Test
@DisplayName("정산 조회는 기간 중 agent 비율 변경이 있어도 거래 시점 기준 비율로 agent 정산금을 계산한다")
fun shouldResolveAgentSettlementRatioAtEventTimeAcrossAllSettlementCategories() {
val agent = saveMember("agent-ratio-history", MemberRole.AGENT)
val creator = saveMember("creator-ratio-history", MemberRole.CREATOR)
val buyer = saveMember("buyer-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, "ratio-content-before", price = 10, settlementRatio = null)
val contentAfter = saveAudioContent(creator, "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 channelBefore = saveChannelDonationUseCan(buyer, 10, LocalDateTime.of(2026, 2, 20, 10, 0, 0))
saveUseCanCalculate(channelBefore, creator.id!!, 10, PaymentGateway.PG)
val channelAfter = saveChannelDonationUseCan(buyer, 20, LocalDateTime.of(2026, 2, 20, 14, 0, 0))
saveUseCanCalculate(channelAfter, creator.id!!, 20, PaymentGateway.PG)
val savedRatios = agentSettlementRatioRepository.findAllByMemberIdOrderByEffectiveFromAsc(agent.id!!)
val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0)
val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59)
val liveRows = repository.getCalculateLiveByCreator(startDate, endDate, agent.id!!)
assertEquals(savedRatios.mapNotNull { it.id }, liveRows.mapNotNull { it.agentSettlementRatioId }.sorted())
val expectedGenericAgentSettlementAmount =
calculateGenericAgentSettlementAmount(totalCan = 10, settlementRatio = 70, agentSettlementRatio = 10) +
calculateGenericAgentSettlementAmount(totalCan = 20, settlementRatio = 70, agentSettlementRatio = 20)
val expectedChannelAgentSettlementAmount =
calculateChannelAgentSettlementAmount(totalCan = 10, agentSettlementRatio = 10) +
calculateChannelAgentSettlementAmount(totalCan = 20, agentSettlementRatio = 20)
val liveResponse = service.getCalculateLiveByCreator("2026-02-20", "2026-02-20", agent.id!!, 0, 10)
assertGenericSettlementResponse(
liveResponse,
expectedCount = 2,
expectedTotalCan = 30,
expectedAgentSettlementAmount = expectedGenericAgentSettlementAmount
)
val contentResponse = service.getCalculateContentByCreator("2026-02-20", "2026-02-20", agent.id!!, 0, 10)
assertGenericSettlementResponse(
contentResponse,
expectedCount = 2,
expectedTotalCan = 30,
expectedAgentSettlementAmount = expectedGenericAgentSettlementAmount
)
val communityResponse = service.getCalculateCommunityByCreator("2026-02-20", "2026-02-20", agent.id!!, 0, 10)
assertGenericSettlementResponse(
communityResponse,
expectedCount = 2,
expectedTotalCan = 30,
expectedAgentSettlementAmount = expectedGenericAgentSettlementAmount
)
val contentDonationResponse = service.getCalculateContentDonationByCreator("2026-02-20", "2026-02-20", agent.id!!, 0, 10)
assertGenericSettlementResponse(
contentDonationResponse,
expectedCount = 2,
expectedTotalCan = 30,
expectedAgentSettlementAmount = expectedGenericAgentSettlementAmount
)
val channelDonationResponse = service.getChannelDonationByCreator("2026-02-20", "2026-02-20", agent.id!!, 0, 10)
assertChannelDonationSettlementResponse(
channelDonationResponse,
expectedCount = 2,
expectedTotalCan = 30,
expectedAgentSettlementAmount = expectedChannelAgentSettlementAmount
)
}
private fun saveMember(nickname: String, role: MemberRole): Member {
return memberRepository.saveAndFlush(
Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
role = role
)
)
}
private fun saveRelation(
agent: Member,
creator: Member,
assignedAt: LocalDateTime = LocalDateTime.of(2026, 2, 1, 0, 0, 0),
unassignedAt: LocalDateTime? = null
) {
val relation = AgentCreatorRelation()
relation.agent = agent
relation.creator = creator
relation.assignedAt = assignedAt
relation.unassignedAt = unassignedAt
relationRepository.saveAndFlush(relation)
}
private fun saveAgentSettlementRatio(
agent: Member,
settlementRatio: Int,
effectiveFrom: LocalDateTime,
effectiveTo: LocalDateTime? = null,
previousEffectiveTo: LocalDateTime? = null
) {
if (previousEffectiveTo != null) {
val previous = agentSettlementRatioRepository.findFirstByMemberIdAndEffectiveToIsNull(agent.id!!)
previous?.effectiveTo = previousEffectiveTo
previous?.let { agentSettlementRatioRepository.saveAndFlush(it) }
}
val ratio = AgentSettlementRatio(
settlementRatio = settlementRatio,
effectiveFrom = effectiveFrom
)
ratio.member = agent
ratio.effectiveTo = effectiveTo
agentSettlementRatioRepository.saveAndFlush(ratio)
}
private fun saveCreatorSettlementRatio(creator: Member, live: Int, content: Int, community: Int) {
val ratio = CreatorSettlementRatio(
subsidy = 0,
liveSettlementRatio = live,
contentSettlementRatio = content,
communitySettlementRatio = community
)
ratio.member = creator
creatorSettlementRatioRepository.saveAndFlush(ratio)
}
private fun saveLiveRoom(creator: Member): LiveRoom {
val room = LiveRoom(
title = "live-room",
notice = "notice",
beginDateTime = LocalDateTime.of(2026, 2, 20, 8, 0, 0),
numberOfPeople = 10,
isAdult = false,
price = 10
)
room.member = creator
return liveRoomRepository.saveAndFlush(room)
}
private fun saveLiveUseCan(sender: Member, room: LiveRoom, can: Int, createdAt: LocalDateTime): UseCan {
val useCan = UseCan(
canUsage = CanUsage.LIVE,
can = can,
rewardCan = 0
)
useCan.member = sender
useCan.room = room
val saved = useCanRepository.saveAndFlush(useCan)
updateUseCanCreatedAt(saved.id!!, createdAt)
return saved
}
private fun saveAudioContent(creator: Member, title: String, price: Int, settlementRatio: Int?): AudioContent {
val theme = AudioContentTheme(
theme = "theme-$title",
image = "image-$title.png"
)
entityManager.persist(theme)
val audioContent = AudioContent(
title = title,
detail = "detail-$title",
languageCode = "ko",
price = price,
settlementRatio = settlementRatio
)
audioContent.theme = theme
audioContent.member = creator
audioContent.isActive = true
return audioContentRepository.saveAndFlush(audioContent)
}
private fun saveOrder(buyer: Member, creator: Member, content: AudioContent, createdAt: LocalDateTime): Order {
val order = Order(type = OrderType.KEEP)
order.member = buyer
order.creator = creator
order.audioContent = content
val saved = orderRepository.saveAndFlush(order)
updateOrderCreatedAt(saved.id!!, createdAt)
return saved
}
private fun saveCommunityPost(creator: Member, price: Int): CreatorCommunity {
val post = CreatorCommunity(
content = "community-content-$price",
price = price,
isCommentAvailable = true,
isAdult = false
)
post.member = creator
return creatorCommunityRepository.saveAndFlush(post)
}
private fun saveCommunityUseCan(buyer: Member, post: CreatorCommunity, can: Int, createdAt: LocalDateTime): UseCan {
val useCan = UseCan(
canUsage = CanUsage.PAID_COMMUNITY_POST,
can = can,
rewardCan = 0
)
useCan.member = buyer
useCan.communityPost = post
val saved = useCanRepository.saveAndFlush(useCan)
updateUseCanCreatedAt(saved.id!!, createdAt)
return saved
}
private fun saveContentDonationUseCan(buyer: Member, content: AudioContent, can: Int, createdAt: LocalDateTime): UseCan {
val useCan = UseCan(
canUsage = CanUsage.DONATION,
can = can,
rewardCan = 0
)
useCan.member = buyer
useCan.audioContent = content
val saved = useCanRepository.saveAndFlush(useCan)
updateUseCanCreatedAt(saved.id!!, createdAt)
return saved
}
private fun saveChannelDonationUseCan(sender: Member, can: Int, createdAt: LocalDateTime): UseCan {
val useCan = UseCan(
canUsage = CanUsage.CHANNEL_DONATION,
can = can,
rewardCan = 0
)
useCan.member = sender
val saved = useCanRepository.saveAndFlush(useCan)
updateUseCanCreatedAt(saved.id!!, createdAt)
return saved
}
private fun saveUseCanCalculate(useCan: UseCan, recipientCreatorId: Long, can: Int, paymentGateway: PaymentGateway) {
val useCanCalculate = UseCanCalculate(
can = can,
paymentGateway = paymentGateway,
status = UseCanCalculateStatus.RECEIVED
)
useCanCalculate.useCan = useCan
useCanCalculate.recipientCreatorId = recipientCreatorId
useCanCalculateRepository.saveAndFlush(useCanCalculate)
}
private fun updateUseCanCreatedAt(useCanId: Long, createdAt: LocalDateTime) {
entityManager.createQuery("update UseCan u set u.createdAt = :createdAt where u.id = :id")
.setParameter("createdAt", createdAt)
.setParameter("id", useCanId)
.executeUpdate()
}
private fun updateOrderCreatedAt(orderId: Long, createdAt: LocalDateTime) {
entityManager.createQuery("update Order o set o.createdAt = :createdAt where o.id = :id")
.setParameter("createdAt", createdAt)
.setParameter("id", orderId)
.executeUpdate()
}
private fun registerMysqlDateFunctions() {
entityManager.createNativeQuery(
"CREATE ALIAS IF NOT EXISTS DATE_FORMAT FOR 'kr.co.vividnext.sodalive.support.H2MysqlDateFunctions.dateFormat'"
).executeUpdate()
entityManager.createNativeQuery(
"CREATE ALIAS IF NOT EXISTS CONVERT_TZ FOR 'kr.co.vividnext.sodalive.support.H2MysqlDateFunctions.convertTz'"
).executeUpdate()
}
private fun assertGenericSettlementResponse(
response: GetAgentSettlementByCreatorResponse,
expectedCount: Int,
expectedTotalCan: Int,
expectedAgentSettlementAmount: Int? = null
) {
assertEquals(1, response.totalCount)
assertEquals(1, response.items.size)
assertEquals(expectedCount, response.total.count)
assertEquals(expectedCount, response.items[0].count)
assertEquals(expectedTotalCan, response.total.totalCan)
assertEquals(expectedTotalCan, response.items[0].totalCan)
expectedAgentSettlementAmount?.let {
assertEquals(it, response.total.agentSettlementAmount)
assertEquals(it, response.items[0].agentSettlementAmount)
}
}
private fun assertChannelDonationSettlementResponse(
response: GetAgentChannelDonationSettlementByCreatorResponse,
expectedCount: Int,
expectedTotalCan: Int,
expectedAgentSettlementAmount: Int? = null
) {
assertEquals(1, response.totalCount)
assertEquals(1, response.items.size)
assertEquals(expectedCount, response.total.count)
assertEquals(expectedCount, response.items[0].count)
assertEquals(expectedTotalCan, response.total.totalCan)
assertEquals(expectedTotalCan, response.items[0].totalCan)
expectedAgentSettlementAmount?.let {
assertEquals(it, response.total.agentSettlementAmount)
assertEquals(it, response.items[0].agentSettlementAmount)
}
}
private fun calculateGenericAgentSettlementAmount(totalCan: Int, settlementRatio: Int, agentSettlementRatio: Int): Int {
val totalKrw = java.math.BigDecimal(totalCan).multiply(java.math.BigDecimal("100"))
val fee = totalKrw.multiply(java.math.BigDecimal("0.066"))
val settlementAmount = totalKrw.subtract(fee)
.multiply(java.math.BigDecimal(settlementRatio).divide(java.math.BigDecimal("100")))
.setScale(0, java.math.RoundingMode.HALF_UP)
.toInt()
return java.math.BigDecimal(settlementAmount)
.multiply(java.math.BigDecimal(agentSettlementRatio).divide(java.math.BigDecimal("100")))
.setScale(0, java.math.RoundingMode.HALF_UP)
.toInt()
}
private fun calculateChannelAgentSettlementAmount(totalCan: Int, agentSettlementRatio: Int): Int {
val settlementAmount = kr.co.vividnext.sodalive.calculate.channelDonation.ChannelDonationSettlementCalculator
.calculate(totalCan)
.settlementAmount
return java.math.BigDecimal(settlementAmount)
.multiply(java.math.BigDecimal(agentSettlementRatio).divide(java.math.BigDecimal("100")))
.setScale(0, java.math.RoundingMode.HALF_UP)
.toInt()
}
}

View File

@@ -0,0 +1,633 @@
package kr.co.vividnext.sodalive.partner.agent.calculate
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
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.AgentSettlementSnapshotType
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import java.time.LocalDateTime
class AgentCalculateServiceTest {
private lateinit var repository: AgentCalculateQueryRepository
private lateinit var snapshotRepository: AgentSettlementSnapshotRepository
private lateinit var service: AgentCalculateService
@BeforeEach
fun setup() {
repository = Mockito.mock(AgentCalculateQueryRepository::class.java)
snapshotRepository = Mockito.mock(
AgentSettlementSnapshotRepository::class.java,
Mockito.withSettings().defaultAnswer { invocation ->
if (invocation.method.name == "findAllByPeriodStartAndPeriodEndAndSettlementTypeAndAgentIdOrderByCreatorIdDesc") {
emptyList<AgentSettlementSnapshot>()
} else {
Mockito.RETURNS_DEFAULTS.answer(invocation)
}
}
)
service = AgentCalculateService(
repository = repository,
snapshotRepository = snapshotRepository
)
}
@Test
@DisplayName("에이전트 서비스는 소속 크리에이터 목록을 페이지 조건으로 조회한다")
fun shouldGetAssignedCreators() {
val items = listOf(
GetAgentAssignedCreatorItem(
creatorId = 21L,
creatorNickname = "creator-a"
)
)
Mockito.`when`(
repository.getAssignedCreatorTotalCount(
Mockito.eq(7L),
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
)
).thenReturn(1)
Mockito.`when`(
repository.getAssignedCreators(
Mockito.eq(7L),
Mockito.eq(10L),
Mockito.eq(5L),
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
)
).thenReturn(items)
val response = service.getAssignedCreators(agentId = 7L, offset = 10L, limit = 5L)
assertEquals(1, response.totalCount)
assertEquals(21L, response.items[0].creatorId)
Mockito.verify(repository).getAssignedCreatorTotalCount(
Mockito.eq(7L),
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
)
Mockito.verify(
repository
).getAssignedCreators(
Mockito.eq(7L),
Mockito.eq(10L),
Mockito.eq(5L),
Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
)
}
@Test
@DisplayName("에이전트 서비스는 라이브 크리에이터별 응답과 합계에 agent 정산금을 계산한다")
fun shouldBuildLiveSummaryResponse() {
val queryData = listOf(
GetAgentCreatorSettlementSummaryQueryData(
creatorId = 21L,
creatorNickname = "creator-a",
count = 2L,
totalCan = 100,
settlementRatio = 70,
agentSettlementRatio = 10
)
)
Mockito.`when`(
repository.getCalculateLiveByCreatorTotalCount(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(1)
Mockito.`when`(
repository.getCalculateLiveByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(queryData)
Mockito.`when`(
repository.getCalculateLiveByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L,
offset = 0L,
limit = 20L
)
).thenReturn(queryData)
val response = service.getCalculateLiveByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 20L
)
assertEquals(1, response.totalCount)
assertEquals(2, response.total.count)
assertEquals(10_000, response.total.krw)
assertEquals(6_538, response.total.settlementAmount)
assertEquals(654, response.total.agentSettlementAmount)
assertEquals(21L, response.items[0].creatorId)
assertEquals(654, response.items[0].agentSettlementAmount)
}
@Test
@DisplayName("에이전트 서비스는 콘텐츠 크리에이터별 응답을 크리에이터 기준으로 병합하고 agent 정산금을 계산한다")
fun shouldMergeContentSummaryRowsPerCreator() {
val totalRows = listOf(
GetAgentCreatorSettlementSummaryQueryData(
creatorId = 21L,
creatorNickname = "creator-a",
count = 1L,
totalCan = 50,
settlementRatio = 80,
agentSettlementRatio = 10
),
GetAgentCreatorSettlementSummaryQueryData(
creatorId = 21L,
creatorNickname = "creator-a",
count = 2L,
totalCan = 30,
settlementRatio = 60,
agentSettlementRatio = 20
)
)
Mockito.`when`(
repository.getCalculateContentByCreatorTotalCount(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(1)
Mockito.`when`(
repository.getCalculateContentByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(totalRows)
Mockito.`when`(
repository.getCalculateContentByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L,
offset = 0L,
limit = 20L
)
).thenReturn(totalRows)
val response = service.getCalculateContentByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 20L
)
assertEquals(1, response.totalCount)
assertEquals(3, response.total.count)
assertEquals(80, response.total.totalCan)
assertEquals(5_417, response.total.settlementAmount)
assertEquals(710, response.total.agentSettlementAmount)
assertEquals(3, response.items[0].count)
assertEquals(80, response.items[0].totalCan)
assertEquals(710, response.items[0].agentSettlementAmount)
}
@Test
@DisplayName("에이전트 서비스는 커뮤니티 크리에이터별 응답과 합계에 agent 정산금을 계산한다")
fun shouldBuildCommunitySummaryResponse() {
val queryData = listOf(
GetAgentCreatorSettlementSummaryQueryData(
creatorId = 21L,
creatorNickname = "creator-a",
count = 2L,
totalCan = 30,
settlementRatio = 60,
agentSettlementRatio = 10
)
)
Mockito.`when`(
repository.getCalculateCommunityByCreatorTotalCount(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(1)
Mockito.`when`(
repository.getCalculateCommunityByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(queryData)
Mockito.`when`(
repository.getCalculateCommunityByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L,
offset = 0L,
limit = 20L
)
).thenReturn(queryData)
val response = service.getCalculateCommunityByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 20L
)
assertEquals(1, response.totalCount)
assertEquals(1_681, response.total.settlementAmount)
assertEquals(168, response.total.agentSettlementAmount)
assertEquals(168, response.items[0].agentSettlementAmount)
}
@Test
@DisplayName("에이전트 서비스는 콘텐츠후원 크리에이터별 응답과 합계에 agent 정산금을 계산한다")
fun shouldBuildContentDonationSummaryResponse() {
val queryData = listOf(
GetAgentCreatorSettlementSummaryQueryData(
creatorId = 21L,
creatorNickname = "creator-a",
count = 2L,
totalCan = 20,
settlementRatio = null,
agentSettlementRatio = 10
)
)
Mockito.`when`(
repository.getCalculateContentDonationByCreatorTotalCount(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(1)
Mockito.`when`(
repository.getCalculateContentDonationByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(queryData)
Mockito.`when`(
repository.getCalculateContentDonationByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L,
offset = 0L,
limit = 20L
)
).thenReturn(queryData)
val response = service.getCalculateContentDonationByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 20L
)
assertEquals(1, response.totalCount)
assertEquals(1_308, response.total.settlementAmount)
assertEquals(131, response.total.agentSettlementAmount)
assertEquals(131, response.items[0].agentSettlementAmount)
}
@Test
@DisplayName("에이전트 비율 이력이 없으면 일반 정산 응답은 10퍼센트 기본값으로 agent 정산금을 계산한다")
fun shouldApplyDefaultAgentSettlementRatioWhenAgentRatioHistoryDoesNotExist() {
val queryData = listOf(
GetAgentCreatorSettlementSummaryQueryData(
creatorId = 21L,
creatorNickname = "creator-a",
count = 2L,
totalCan = 20,
settlementRatio = null,
agentSettlementRatio = null
)
)
Mockito.`when`(
repository.getCalculateContentDonationByCreatorTotalCount(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(1)
Mockito.`when`(
repository.getCalculateContentDonationByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(queryData)
Mockito.`when`(
repository.getCalculateContentDonationByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L,
offset = 0L,
limit = 20L
)
).thenReturn(queryData)
val response = service.getCalculateContentDonationByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 20L
)
assertEquals(1, response.totalCount)
assertEquals(1_308, response.total.settlementAmount)
assertEquals(131, response.total.agentSettlementAmount)
assertEquals(131, response.items[0].agentSettlementAmount)
}
@Test
@DisplayName("에이전트 서비스는 채널후원 크리에이터별 응답과 합계에 agent 정산금을 계산한다")
fun shouldBuildChannelDonationSummaryResponse() {
val queryData = listOf(
GetAgentChannelDonationSettlementByCreatorQueryData(
creatorId = 21L,
creatorNickname = "creator-a",
count = 1L,
totalCan = 50,
agentSettlementRatio = 10
)
)
Mockito.`when`(
repository.getChannelDonationByCreatorTotalCount(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(1)
Mockito.`when`(
repository.getChannelDonationByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(queryData)
Mockito.`when`(
repository.getChannelDonationByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L,
offset = 0L,
limit = 20L
)
).thenReturn(queryData)
val response = service.getChannelDonationByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 20L
)
assertEquals(1, response.totalCount)
assertEquals(3_970, response.total.settlementAmount)
assertEquals(397, response.total.agentSettlementAmount)
assertEquals(397, response.items[0].agentSettlementAmount)
}
@Test
@DisplayName("에이전트 비율 이력이 없으면 채널후원 응답은 10퍼센트 기본값으로 agent 정산금을 계산한다")
fun shouldApplyDefaultAgentSettlementRatioToChannelDonationWhenAgentRatioHistoryDoesNotExist() {
val queryData = listOf(
GetAgentChannelDonationSettlementByCreatorQueryData(
creatorId = 21L,
creatorNickname = "creator-a",
count = 1L,
totalCan = 50,
agentSettlementRatio = null
)
)
Mockito.`when`(
repository.getChannelDonationByCreatorTotalCount(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(1)
Mockito.`when`(
repository.getChannelDonationByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L
)
).thenReturn(queryData)
Mockito.`when`(
repository.getChannelDonationByCreator(
startDate = "2026-02-20".convertLocalDateTime(),
endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
agentId = 7L,
offset = 0L,
limit = 20L
)
).thenReturn(queryData)
val response = service.getChannelDonationByCreator(
startDateStr = "2026-02-20",
endDateStr = "2026-02-21",
agentId = 7L,
offset = 0L,
limit = 20L
)
assertEquals(1, response.totalCount)
assertEquals(3_970, response.total.settlementAmount)
assertEquals(397, response.total.agentSettlementAmount)
assertEquals(397, response.items[0].agentSettlementAmount)
}
@Test
@DisplayName("에이전트 서비스는 finalized 기간이면 다섯 카테고리 모두 스냅샷을 우선 사용한다")
fun shouldUseFinalizedSnapshotsFirstAcrossAllSettlementCategories() {
val startDate = "2026-02-20".convertLocalDateTime()
val endDate = "2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59)
Mockito.`when`(
snapshotRepository.findAllByPeriodStartAndPeriodEndAndSettlementTypeAndAgentIdOrderByCreatorIdDesc(
startDate,
endDate,
AgentSettlementSnapshotType.LIVE,
7L
)
).thenReturn(
listOf(
createSnapshot(
settlementType = AgentSettlementSnapshotType.LIVE,
count = 2,
totalCan = 100,
krw = 10_000,
fee = 660,
settlementAmount = 6_538,
tax = 216,
depositAmount = 6_322,
agentSettlementAmount = 654,
appliedAgentSettlementRatio = 10
)
)
)
Mockito.`when`(
snapshotRepository.findAllByPeriodStartAndPeriodEndAndSettlementTypeAndAgentIdOrderByCreatorIdDesc(
startDate,
endDate,
AgentSettlementSnapshotType.CONTENT,
7L
)
).thenReturn(
listOf(
createSnapshot(
settlementType = AgentSettlementSnapshotType.CONTENT,
count = 3,
totalCan = 80,
krw = 8_000,
fee = 528,
settlementAmount = 5_417,
tax = 179,
depositAmount = 5_238,
agentSettlementAmount = 710,
appliedAgentSettlementRatio = null
)
)
)
Mockito.`when`(
snapshotRepository.findAllByPeriodStartAndPeriodEndAndSettlementTypeAndAgentIdOrderByCreatorIdDesc(
startDate,
endDate,
AgentSettlementSnapshotType.COMMUNITY,
7L
)
).thenReturn(
listOf(
createSnapshot(
settlementType = AgentSettlementSnapshotType.COMMUNITY,
count = 2,
totalCan = 30,
krw = 3_000,
fee = 198,
settlementAmount = 1_681,
tax = 55,
depositAmount = 1_626,
agentSettlementAmount = 168,
appliedAgentSettlementRatio = 10
)
)
)
Mockito.`when`(
snapshotRepository.findAllByPeriodStartAndPeriodEndAndSettlementTypeAndAgentIdOrderByCreatorIdDesc(
startDate,
endDate,
AgentSettlementSnapshotType.CONTENT_DONATION,
7L
)
).thenReturn(
listOf(
createSnapshot(
settlementType = AgentSettlementSnapshotType.CONTENT_DONATION,
count = 2,
totalCan = 20,
krw = 2_000,
fee = 132,
settlementAmount = 1_308,
tax = 43,
depositAmount = 1_265,
agentSettlementAmount = 131,
appliedAgentSettlementRatio = 10
)
)
)
Mockito.`when`(
snapshotRepository.findAllByPeriodStartAndPeriodEndAndSettlementTypeAndAgentIdOrderByCreatorIdDesc(
startDate,
endDate,
AgentSettlementSnapshotType.CHANNEL_DONATION,
7L
)
).thenReturn(
listOf(
createSnapshot(
settlementType = AgentSettlementSnapshotType.CHANNEL_DONATION,
count = 1,
totalCan = 50,
krw = 5_000,
fee = 330,
settlementAmount = 3_970,
tax = 131,
depositAmount = 3_839,
agentSettlementAmount = 397,
appliedAgentSettlementRatio = 10
)
)
)
val liveResponse = service.getCalculateLiveByCreator("2026-02-20", "2026-02-21", 7L, 0L, 20L)
val contentResponse = service.getCalculateContentByCreator("2026-02-20", "2026-02-21", 7L, 0L, 20L)
val communityResponse = service.getCalculateCommunityByCreator("2026-02-20", "2026-02-21", 7L, 0L, 20L)
val contentDonationResponse = service.getCalculateContentDonationByCreator("2026-02-20", "2026-02-21", 7L, 0L, 20L)
val channelDonationResponse = service.getChannelDonationByCreator("2026-02-20", "2026-02-21", 7L, 0L, 20L)
assertEquals(654, liveResponse.total.agentSettlementAmount)
assertEquals(710, contentResponse.total.agentSettlementAmount)
assertEquals(168, communityResponse.total.agentSettlementAmount)
assertEquals(131, contentDonationResponse.total.agentSettlementAmount)
assertEquals(397, channelDonationResponse.total.agentSettlementAmount)
assertEquals(6_538, liveResponse.items[0].settlementAmount)
assertEquals(5_417, contentResponse.items[0].settlementAmount)
assertEquals(1_681, communityResponse.items[0].settlementAmount)
assertEquals(1_308, contentDonationResponse.items[0].settlementAmount)
assertEquals(3_970, channelDonationResponse.items[0].settlementAmount)
assertEquals(131, channelDonationResponse.items[0].withholdingTax)
Mockito.verifyNoInteractions(repository)
}
private fun createSnapshot(
settlementType: AgentSettlementSnapshotType,
count: Int,
totalCan: Int,
krw: Int,
fee: Int,
settlementAmount: Int,
tax: Int,
depositAmount: Int,
agentSettlementAmount: Int,
appliedAgentSettlementRatio: Int?
): AgentSettlementSnapshot {
return AgentSettlementSnapshot(
periodStart = LocalDateTime.of(2026, 2, 20, 0, 0, 0),
periodEnd = LocalDateTime.of(2026, 2, 21, 23, 59, 59),
settlementType = settlementType,
agentId = 7L,
agentNickname = "agent-a",
creatorId = 21L,
creatorNickname = "creator-a",
appliedAgentSettlementRatio = appliedAgentSettlementRatio,
count = count,
totalCan = totalCan,
krw = krw,
fee = fee,
settlementAmount = settlementAmount,
tax = tax,
depositAmount = depositAmount,
agentSettlementAmount = agentSettlementAmount,
finalizedAt = LocalDateTime.of(2026, 2, 22, 0, 0, 0),
finalizedByMemberId = 1L
)
}
}