feat(channel-donation-calculate): 채널 후원 정산 조회 기능을 추가한다
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
|
||||
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
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('ADMIN')")
|
||||
@RequestMapping("/admin/calculate")
|
||||
class AdminChannelDonationCalculateController(
|
||||
private val service: AdminChannelDonationCalculateService
|
||||
) {
|
||||
@GetMapping("/channel-donation-by-creator")
|
||||
fun getChannelDonationByCreator(
|
||||
@RequestParam startDateStr: String,
|
||||
@RequestParam endDateStr: String,
|
||||
pageable: Pageable
|
||||
) = ApiResponse.ok(
|
||||
service.getChannelDonationByCreator(
|
||||
startDateStr = startDateStr,
|
||||
endDateStr = endDateStr,
|
||||
offset = pageable.offset,
|
||||
limit = pageable.pageSize.toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
|
||||
|
||||
import com.querydsl.core.types.dsl.DateTimePath
|
||||
import com.querydsl.core.types.dsl.Expressions
|
||||
import com.querydsl.core.types.dsl.StringTemplate
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
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.member.QMember.member
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
class AdminChannelDonationCalculateQueryRepository(
|
||||
private val queryFactory: JPAQueryFactory
|
||||
) {
|
||||
fun getChannelDonationByCreatorTotalCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
|
||||
val formattedDate = getFormattedDate(useCan.createdAt)
|
||||
val distinctGroupKey = Expressions.stringTemplate(
|
||||
"CONCAT({0}, '-', {1})",
|
||||
formattedDate,
|
||||
member.id.stringValue()
|
||||
)
|
||||
|
||||
return queryFactory
|
||||
.select(distinctGroupKey.countDistinct())
|
||||
.from(useCanCalculate)
|
||||
.innerJoin(useCanCalculate.useCan, useCan)
|
||||
.innerJoin(member)
|
||||
.on(member.id.eq(useCanCalculate.recipientCreatorId))
|
||||
.where(baseWhereCondition(startDate, endDate))
|
||||
.fetchOne()
|
||||
?.toInt()
|
||||
?: 0
|
||||
}
|
||||
|
||||
fun getChannelDonationByCreator(
|
||||
startDate: LocalDateTime,
|
||||
endDate: LocalDateTime,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<GetAdminChannelDonationSettlementQueryData> {
|
||||
val formattedDate = getFormattedDate(useCan.createdAt)
|
||||
|
||||
return queryFactory
|
||||
.select(
|
||||
QGetAdminChannelDonationSettlementQueryData(
|
||||
formattedDate,
|
||||
member.nickname,
|
||||
useCan.id.countDistinct(),
|
||||
useCanCalculate.can.sum()
|
||||
)
|
||||
)
|
||||
.from(useCanCalculate)
|
||||
.innerJoin(useCanCalculate.useCan, useCan)
|
||||
.innerJoin(member)
|
||||
.on(member.id.eq(useCanCalculate.recipientCreatorId))
|
||||
.where(baseWhereCondition(startDate, endDate))
|
||||
.groupBy(formattedDate, member.id)
|
||||
.orderBy(formattedDate.desc(), member.id.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.fetch()
|
||||
}
|
||||
|
||||
private fun baseWhereCondition(
|
||||
startDate: LocalDateTime,
|
||||
endDate: LocalDateTime
|
||||
) = 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))
|
||||
|
||||
private fun getFormattedDate(dateTimePath: DateTimePath<LocalDateTime>): StringTemplate {
|
||||
return Expressions.stringTemplate(
|
||||
"DATE_FORMAT({0}, {1})",
|
||||
Expressions.dateTimeTemplate(
|
||||
LocalDateTime::class.java,
|
||||
"CONVERT_TZ({0},{1},{2})",
|
||||
dateTimePath,
|
||||
"UTC",
|
||||
"Asia/Seoul"
|
||||
),
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
|
||||
|
||||
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Service
|
||||
class AdminChannelDonationCalculateService(
|
||||
private val repository: AdminChannelDonationCalculateQueryRepository
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
fun getChannelDonationByCreator(
|
||||
startDateStr: String,
|
||||
endDateStr: String,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): GetAdminChannelDonationSettlementResponse {
|
||||
val startDate = startDateStr.convertLocalDateTime()
|
||||
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
|
||||
|
||||
val totalCount = repository.getChannelDonationByCreatorTotalCount(startDate, endDate)
|
||||
val items = repository
|
||||
.getChannelDonationByCreator(startDate, endDate, offset, limit)
|
||||
.map { it.toResponseItem() }
|
||||
|
||||
return GetAdminChannelDonationSettlementResponse(totalCount, items)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
data class GetAdminChannelDonationSettlementItem(
|
||||
@JsonProperty("date") val date: String,
|
||||
@JsonProperty("creator") val creator: String,
|
||||
@JsonProperty("count") val count: Int,
|
||||
@JsonProperty("totalCan") val totalCan: Int,
|
||||
@JsonProperty("krw") val krw: Int,
|
||||
@JsonProperty("fee") val fee: Int,
|
||||
@JsonProperty("settlementAmount") val settlementAmount: Int,
|
||||
@JsonProperty("withholdingTax") val withholdingTax: Int,
|
||||
@JsonProperty("depositAmount") val depositAmount: Int
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
|
||||
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
import kr.co.vividnext.sodalive.calculate.channelDonation.ChannelDonationSettlementCalculator
|
||||
|
||||
data class GetAdminChannelDonationSettlementQueryData @QueryProjection constructor(
|
||||
val date: String,
|
||||
val creator: String,
|
||||
val count: Long,
|
||||
val totalCan: Int?
|
||||
) {
|
||||
fun toResponseItem(): GetAdminChannelDonationSettlementItem {
|
||||
val settlement = ChannelDonationSettlementCalculator.calculate(totalCan ?: 0)
|
||||
|
||||
return GetAdminChannelDonationSettlementItem(
|
||||
date = date,
|
||||
creator = creator,
|
||||
count = count.toInt(),
|
||||
totalCan = totalCan ?: 0,
|
||||
krw = settlement.krw,
|
||||
fee = settlement.fee,
|
||||
settlementAmount = settlement.settlementAmount,
|
||||
withholdingTax = settlement.withholdingTax,
|
||||
depositAmount = settlement.depositAmount
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
|
||||
|
||||
data class GetAdminChannelDonationSettlementResponse(
|
||||
val totalCount: Int,
|
||||
val items: List<GetAdminChannelDonationSettlementItem>
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
package kr.co.vividnext.sodalive.calculate.channelDonation
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
data class ChannelDonationSettlementAmount(
|
||||
val krw: Int,
|
||||
val fee: Int,
|
||||
val settlementAmount: Int,
|
||||
val withholdingTax: Int,
|
||||
val depositAmount: Int
|
||||
)
|
||||
|
||||
object ChannelDonationSettlementCalculator {
|
||||
private val KRW_PER_CAN = BigDecimal("100")
|
||||
private val FEE_RATE = BigDecimal("0.066")
|
||||
private val SETTLEMENT_RATE = BigDecimal("0.85")
|
||||
private val WITHHOLDING_TAX_RATE = BigDecimal("0.033")
|
||||
|
||||
fun calculate(totalCan: Int): ChannelDonationSettlementAmount {
|
||||
// 원화 = 캔 * 100
|
||||
val krw = BigDecimal(totalCan).multiply(KRW_PER_CAN).setScale(0, RoundingMode.HALF_UP).toInt()
|
||||
|
||||
// 수수료 = 원화 * 6.6%
|
||||
val fee = BigDecimal(krw).multiply(FEE_RATE).setScale(0, RoundingMode.HALF_UP).toInt()
|
||||
|
||||
// 정산금액 = (원화 - 수수료) * 85%
|
||||
val settlementAmount = BigDecimal(krw - fee)
|
||||
.multiply(SETTLEMENT_RATE)
|
||||
.setScale(0, RoundingMode.HALF_UP)
|
||||
.toInt()
|
||||
|
||||
// 원천세 = 정산금액 * 3.3%
|
||||
val withholdingTax = BigDecimal(settlementAmount)
|
||||
.multiply(WITHHOLDING_TAX_RATE)
|
||||
.setScale(0, RoundingMode.HALF_UP)
|
||||
.toInt()
|
||||
|
||||
// 입금액 = 정산금액 - 원천세
|
||||
val depositAmount = settlementAmount - withholdingTax
|
||||
|
||||
return ChannelDonationSettlementAmount(
|
||||
krw = krw,
|
||||
fee = fee,
|
||||
settlementAmount = settlementAmount,
|
||||
withholdingTax = withholdingTax,
|
||||
depositAmount = depositAmount
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package kr.co.vividnext.sodalive.creator.admin.calculate.channelDonation
|
||||
|
||||
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('CREATOR')")
|
||||
@RequestMapping("/creator-admin/calculate")
|
||||
class CreatorAdminChannelDonationCalculateController(
|
||||
private val service: CreatorAdminChannelDonationCalculateService
|
||||
) {
|
||||
@GetMapping("/channel-donation")
|
||||
fun getChannelDonation(
|
||||
@RequestParam startDateStr: String,
|
||||
@RequestParam endDateStr: String,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||
pageable: Pageable
|
||||
) = run {
|
||||
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
|
||||
|
||||
ApiResponse.ok(
|
||||
service.getChannelDonation(
|
||||
startDateStr = startDateStr,
|
||||
endDateStr = endDateStr,
|
||||
memberId = member.id!!,
|
||||
creatorNickname = member.nickname,
|
||||
offset = pageable.offset,
|
||||
limit = pageable.pageSize.toLong()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package kr.co.vividnext.sodalive.creator.admin.calculate.channelDonation
|
||||
|
||||
import com.querydsl.core.types.dsl.DateTimePath
|
||||
import com.querydsl.core.types.dsl.Expressions
|
||||
import com.querydsl.core.types.dsl.StringTemplate
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
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 org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
class CreatorAdminChannelDonationCalculateQueryRepository(
|
||||
private val queryFactory: JPAQueryFactory
|
||||
) {
|
||||
fun getChannelDonationTotalCount(startDate: LocalDateTime, endDate: LocalDateTime, memberId: Long): Int {
|
||||
val formattedDate = getFormattedDate(useCan.createdAt)
|
||||
|
||||
return queryFactory
|
||||
.select(formattedDate.countDistinct())
|
||||
.from(useCanCalculate)
|
||||
.innerJoin(useCanCalculate.useCan, useCan)
|
||||
.where(baseWhereCondition(startDate, endDate, memberId))
|
||||
.fetchOne()
|
||||
?.toInt()
|
||||
?: 0
|
||||
}
|
||||
|
||||
fun getChannelDonation(
|
||||
startDate: LocalDateTime,
|
||||
endDate: LocalDateTime,
|
||||
memberId: Long,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): List<GetCreatorChannelDonationSettlementQueryData> {
|
||||
val formattedDate = getFormattedDate(useCan.createdAt)
|
||||
|
||||
return queryFactory
|
||||
.select(
|
||||
QGetCreatorChannelDonationSettlementQueryData(
|
||||
formattedDate,
|
||||
useCan.id.countDistinct(),
|
||||
useCanCalculate.can.sum()
|
||||
)
|
||||
)
|
||||
.from(useCanCalculate)
|
||||
.innerJoin(useCanCalculate.useCan, useCan)
|
||||
.where(baseWhereCondition(startDate, endDate, memberId))
|
||||
.groupBy(formattedDate)
|
||||
.orderBy(formattedDate.desc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
.fetch()
|
||||
}
|
||||
|
||||
private fun baseWhereCondition(startDate: LocalDateTime, endDate: LocalDateTime, memberId: Long) = useCan.canUsage.eq(
|
||||
CanUsage.CHANNEL_DONATION
|
||||
)
|
||||
.and(useCan.isRefund.isFalse)
|
||||
.and(useCanCalculate.status.eq(UseCanCalculateStatus.RECEIVED))
|
||||
.and(useCanCalculate.recipientCreatorId.eq(memberId))
|
||||
.and(useCan.createdAt.goe(startDate))
|
||||
.and(useCan.createdAt.loe(endDate))
|
||||
|
||||
private fun getFormattedDate(dateTimePath: DateTimePath<LocalDateTime>): StringTemplate {
|
||||
return Expressions.stringTemplate(
|
||||
"DATE_FORMAT({0}, {1})",
|
||||
Expressions.dateTimeTemplate(
|
||||
LocalDateTime::class.java,
|
||||
"CONVERT_TZ({0},{1},{2})",
|
||||
dateTimePath,
|
||||
"UTC",
|
||||
"Asia/Seoul"
|
||||
),
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package kr.co.vividnext.sodalive.creator.admin.calculate.channelDonation
|
||||
|
||||
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Service
|
||||
class CreatorAdminChannelDonationCalculateService(
|
||||
private val repository: CreatorAdminChannelDonationCalculateQueryRepository
|
||||
) {
|
||||
@Transactional(readOnly = true)
|
||||
fun getChannelDonation(
|
||||
startDateStr: String,
|
||||
endDateStr: String,
|
||||
memberId: Long,
|
||||
creatorNickname: String,
|
||||
offset: Long,
|
||||
limit: Long
|
||||
): GetCreatorChannelDonationSettlementResponse {
|
||||
val startDate = startDateStr.convertLocalDateTime()
|
||||
val endDate = endDateStr.convertLocalDateTime(hour = 23, minute = 59, second = 59)
|
||||
|
||||
val totalCount = repository.getChannelDonationTotalCount(startDate, endDate, memberId)
|
||||
val items = repository
|
||||
.getChannelDonation(startDate, endDate, memberId, offset, limit)
|
||||
.map { it.toResponseItem(creatorNickname) }
|
||||
|
||||
return GetCreatorChannelDonationSettlementResponse(totalCount, items)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package kr.co.vividnext.sodalive.creator.admin.calculate.channelDonation
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
data class GetCreatorChannelDonationSettlementItem(
|
||||
@JsonProperty("date") val date: String,
|
||||
@JsonProperty("creator") val creator: String,
|
||||
@JsonProperty("count") val count: Int,
|
||||
@JsonProperty("totalCan") val totalCan: Int,
|
||||
@JsonProperty("krw") val krw: Int,
|
||||
@JsonProperty("fee") val fee: Int,
|
||||
@JsonProperty("settlementAmount") val settlementAmount: Int,
|
||||
@JsonProperty("withholdingTax") val withholdingTax: Int,
|
||||
@JsonProperty("depositAmount") val depositAmount: Int
|
||||
)
|
||||
@@ -0,0 +1,26 @@
|
||||
package kr.co.vividnext.sodalive.creator.admin.calculate.channelDonation
|
||||
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
import kr.co.vividnext.sodalive.calculate.channelDonation.ChannelDonationSettlementCalculator
|
||||
|
||||
data class GetCreatorChannelDonationSettlementQueryData @QueryProjection constructor(
|
||||
val date: String,
|
||||
val count: Long,
|
||||
val totalCan: Int?
|
||||
) {
|
||||
fun toResponseItem(creatorNickname: String): GetCreatorChannelDonationSettlementItem {
|
||||
val settlement = ChannelDonationSettlementCalculator.calculate(totalCan ?: 0)
|
||||
|
||||
return GetCreatorChannelDonationSettlementItem(
|
||||
date = date,
|
||||
creator = creatorNickname,
|
||||
count = count.toInt(),
|
||||
totalCan = totalCan ?: 0,
|
||||
krw = settlement.krw,
|
||||
fee = settlement.fee,
|
||||
settlementAmount = settlement.settlementAmount,
|
||||
withholdingTax = settlement.withholdingTax,
|
||||
depositAmount = settlement.depositAmount
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.creator.admin.calculate.channelDonation
|
||||
|
||||
data class GetCreatorChannelDonationSettlementResponse(
|
||||
val totalCount: Int,
|
||||
val items: List<GetCreatorChannelDonationSettlementItem>
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
|
||||
|
||||
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 org.springframework.data.domain.PageRequest
|
||||
|
||||
class AdminChannelDonationCalculateControllerTest {
|
||||
private lateinit var service: AdminChannelDonationCalculateService
|
||||
private lateinit var controller: AdminChannelDonationCalculateController
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
service = Mockito.mock(AdminChannelDonationCalculateService::class.java)
|
||||
controller = AdminChannelDonationCalculateController(service)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("관리자 컨트롤러는 날짜/페이지 파라미터를 서비스로 전달한다")
|
||||
fun shouldForwardDateRangeAndPageableToService() {
|
||||
val response = GetAdminChannelDonationSettlementResponse(
|
||||
totalCount = 1,
|
||||
items = listOf(
|
||||
GetAdminChannelDonationSettlementItem(
|
||||
date = "2026-02-26",
|
||||
creator = "creator-a",
|
||||
count = 2,
|
||||
totalCan = 20,
|
||||
krw = 2000,
|
||||
fee = 132,
|
||||
settlementAmount = 1588,
|
||||
withholdingTax = 52,
|
||||
depositAmount = 1536
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Mockito.`when`(
|
||||
service.getChannelDonationByCreator(
|
||||
startDateStr = "2026-02-20",
|
||||
endDateStr = "2026-02-21",
|
||||
offset = 15L,
|
||||
limit = 15L
|
||||
)
|
||||
).thenReturn(response)
|
||||
|
||||
val apiResponse = controller.getChannelDonationByCreator(
|
||||
startDateStr = "2026-02-20",
|
||||
endDateStr = "2026-02-21",
|
||||
pageable = PageRequest.of(1, 15)
|
||||
)
|
||||
|
||||
assertEquals(true, apiResponse.success)
|
||||
assertEquals(1, apiResponse.data!!.totalCount)
|
||||
assertEquals("creator-a", apiResponse.data!!.items[0].creator)
|
||||
assertEquals(2, apiResponse.data!!.items[0].count)
|
||||
assertEquals(20, apiResponse.data!!.items[0].totalCan)
|
||||
|
||||
Mockito.verify(service).getChannelDonationByCreator(
|
||||
startDateStr = "2026-02-20",
|
||||
endDateStr = "2026-02-21",
|
||||
offset = 15L,
|
||||
limit = 15L
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
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.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
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 AdminChannelDonationCalculateQueryRepositoryTest @Autowired constructor(
|
||||
private val queryFactory: JPAQueryFactory,
|
||||
private val memberRepository: MemberRepository,
|
||||
private val useCanRepository: UseCanRepository,
|
||||
private val useCanCalculateRepository: UseCanCalculateRepository,
|
||||
private val entityManager: EntityManager
|
||||
) {
|
||||
private lateinit var repository: AdminChannelDonationCalculateQueryRepository
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
registerMysqlDateFunctions()
|
||||
repository = AdminChannelDonationCalculateQueryRepository(queryFactory)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("동일 후원의 분할 정산 레코드는 건수를 중복 집계하지 않는다")
|
||||
fun shouldCountDistinctUseCanWhenDonationIsSplitAcrossCalculations() {
|
||||
val creator = saveMember(nickname = "creator-admin", role = MemberRole.CREATOR)
|
||||
val sender = saveMember(nickname = "sender-admin", role = MemberRole.USER)
|
||||
val useCan = saveUseCan(member = sender, can = 50, rewardCan = 0)
|
||||
|
||||
saveUseCanCalculate(
|
||||
useCan = useCan,
|
||||
recipientCreatorId = creator.id!!,
|
||||
can = 20,
|
||||
paymentGateway = PaymentGateway.PG
|
||||
)
|
||||
saveUseCanCalculate(
|
||||
useCan = useCan,
|
||||
recipientCreatorId = creator.id!!,
|
||||
can = 30,
|
||||
paymentGateway = PaymentGateway.GOOGLE_IAP
|
||||
)
|
||||
updateUseCanCreatedAt(useCan.id!!, LocalDateTime.of(2026, 2, 20, 12, 0, 0))
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
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)
|
||||
val items = repository.getChannelDonationByCreator(startDate, endDate, offset = 0, limit = 20)
|
||||
|
||||
assertEquals(1, totalCount)
|
||||
assertEquals(1, items.size)
|
||||
assertEquals("2026-02-20", items[0].date)
|
||||
assertEquals("creator-admin", items[0].creator)
|
||||
assertEquals(1L, items[0].count)
|
||||
assertEquals(50, items[0].totalCan)
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole): Member {
|
||||
return memberRepository.saveAndFlush(
|
||||
Member(
|
||||
email = "$nickname@test.com",
|
||||
password = "password",
|
||||
nickname = nickname,
|
||||
role = role
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveUseCan(member: Member, can: Int, rewardCan: Int): UseCan {
|
||||
val useCan = UseCan(
|
||||
canUsage = CanUsage.CHANNEL_DONATION,
|
||||
can = can,
|
||||
rewardCan = rewardCan
|
||||
)
|
||||
useCan.member = member
|
||||
return useCanRepository.saveAndFlush(useCan)
|
||||
}
|
||||
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package kr.co.vividnext.sodalive.admin.calculate.channelDonation
|
||||
|
||||
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
|
||||
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
|
||||
|
||||
class AdminChannelDonationCalculateServiceTest {
|
||||
private lateinit var repository: AdminChannelDonationCalculateQueryRepository
|
||||
private lateinit var service: AdminChannelDonationCalculateService
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
repository = Mockito.mock(AdminChannelDonationCalculateQueryRepository::class.java)
|
||||
service = AdminChannelDonationCalculateService(repository)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("관리자 정산 조회는 날짜 범위를 변환하고 크리에이터 그룹 응답을 반환한다")
|
||||
fun shouldConvertDateRangeAndReturnCreatorGroupedItems() {
|
||||
val queryData = GetAdminChannelDonationSettlementQueryData(
|
||||
date = "2026-02-26",
|
||||
creator = "creator-a",
|
||||
count = 3L,
|
||||
totalCan = 100
|
||||
)
|
||||
|
||||
Mockito.`when`(
|
||||
repository.getChannelDonationByCreatorTotalCount(
|
||||
"2026-02-20".convertLocalDateTime(),
|
||||
"2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59)
|
||||
)
|
||||
).thenReturn(1)
|
||||
Mockito.`when`(
|
||||
repository.getChannelDonationByCreator(
|
||||
"2026-02-20".convertLocalDateTime(),
|
||||
"2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
|
||||
0L,
|
||||
20L
|
||||
)
|
||||
).thenReturn(listOf(queryData))
|
||||
|
||||
val result = service.getChannelDonationByCreator(
|
||||
startDateStr = "2026-02-20",
|
||||
endDateStr = "2026-02-21",
|
||||
offset = 0,
|
||||
limit = 20
|
||||
)
|
||||
|
||||
assertEquals(1, result.totalCount)
|
||||
assertEquals(1, result.items.size)
|
||||
assertEquals("2026-02-26", result.items[0].date)
|
||||
assertEquals("creator-a", result.items[0].creator)
|
||||
assertEquals(3, result.items[0].count)
|
||||
assertEquals(100, result.items[0].totalCan)
|
||||
assertEquals(10_000, result.items[0].krw)
|
||||
assertEquals(660, result.items[0].fee)
|
||||
|
||||
Mockito.verify(repository).getChannelDonationByCreatorTotalCount(
|
||||
"2026-02-20".convertLocalDateTime(),
|
||||
"2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59)
|
||||
)
|
||||
Mockito.verify(repository).getChannelDonationByCreator(
|
||||
"2026-02-20".convertLocalDateTime(),
|
||||
"2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
|
||||
0L,
|
||||
20L
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package kr.co.vividnext.sodalive.calculate.channelDonation
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ChannelDonationSettlementCalculatorTest {
|
||||
@Test
|
||||
@DisplayName("정산 공식에 따라 금액을 계산한다")
|
||||
fun shouldCalculateSettlementAmountsWithExpectedFormula() {
|
||||
val result = ChannelDonationSettlementCalculator.calculate(totalCan = 100)
|
||||
|
||||
assertEquals(10_000, result.krw)
|
||||
assertEquals(660, result.fee)
|
||||
assertEquals(7_939, result.settlementAmount)
|
||||
assertEquals(262, result.withholdingTax)
|
||||
assertEquals(7_677, result.depositAmount)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("소수 계산은 단계별 반올림 규칙을 따른다")
|
||||
fun shouldRoundHalfUpOnDecimalResults() {
|
||||
val result = ChannelDonationSettlementCalculator.calculate(totalCan = 1)
|
||||
|
||||
assertEquals(100, result.krw)
|
||||
assertEquals(7, result.fee)
|
||||
assertEquals(79, result.settlementAmount)
|
||||
assertEquals(3, result.withholdingTax)
|
||||
assertEquals(76, result.depositAmount)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("총 캔 수가 0이면 모든 금액은 0이다")
|
||||
fun shouldReturnZeroWhenTotalCanIsZero() {
|
||||
val result = ChannelDonationSettlementCalculator.calculate(totalCan = 0)
|
||||
|
||||
assertEquals(0, result.krw)
|
||||
assertEquals(0, result.fee)
|
||||
assertEquals(0, result.settlementAmount)
|
||||
assertEquals(0, result.withholdingTax)
|
||||
assertEquals(0, result.depositAmount)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package kr.co.vividnext.sodalive.creator.admin.calculate.channelDonation
|
||||
|
||||
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 CreatorAdminChannelDonationCalculateControllerTest {
|
||||
private lateinit var service: CreatorAdminChannelDonationCalculateService
|
||||
private lateinit var controller: CreatorAdminChannelDonationCalculateController
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
service = Mockito.mock(CreatorAdminChannelDonationCalculateService::class.java)
|
||||
controller = CreatorAdminChannelDonationCalculateController(service)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("인증 사용자 정보가 없으면 예외를 던진다")
|
||||
fun shouldThrowWhenMemberIsNull() {
|
||||
val exception = assertThrows(SodaException::class.java) {
|
||||
controller.getChannelDonation(
|
||||
startDateStr = "2026-02-20",
|
||||
endDateStr = "2026-02-21",
|
||||
member = null,
|
||||
pageable = PageRequest.of(0, 10)
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("common.error.bad_credentials", exception.messageKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("크리에이터 컨트롤러는 본인 ID/닉네임과 페이지 정보를 서비스로 전달한다")
|
||||
fun shouldForwardMemberAndPageableToService() {
|
||||
val member = createMember(7L)
|
||||
val response = GetCreatorChannelDonationSettlementResponse(
|
||||
totalCount = 1,
|
||||
items = listOf(
|
||||
GetCreatorChannelDonationSettlementItem(
|
||||
date = "2026-02-26",
|
||||
creator = "creator-self",
|
||||
count = 4,
|
||||
totalCan = 10,
|
||||
krw = 1000,
|
||||
fee = 66,
|
||||
settlementAmount = 794,
|
||||
withholdingTax = 26,
|
||||
depositAmount = 768
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Mockito.`when`(
|
||||
service.getChannelDonation(
|
||||
startDateStr = "2026-02-20",
|
||||
endDateStr = "2026-02-21",
|
||||
memberId = 7L,
|
||||
creatorNickname = "creator",
|
||||
offset = 10L,
|
||||
limit = 5L
|
||||
)
|
||||
).thenReturn(response)
|
||||
|
||||
val apiResponse = controller.getChannelDonation(
|
||||
startDateStr = "2026-02-20",
|
||||
endDateStr = "2026-02-21",
|
||||
member = member,
|
||||
pageable = PageRequest.of(2, 5)
|
||||
)
|
||||
|
||||
assertEquals(true, apiResponse.success)
|
||||
assertEquals(1, apiResponse.data!!.totalCount)
|
||||
assertEquals("creator-self", apiResponse.data!!.items[0].creator)
|
||||
assertEquals(4, apiResponse.data!!.items[0].count)
|
||||
assertEquals(10, apiResponse.data!!.items[0].totalCan)
|
||||
|
||||
Mockito.verify(service).getChannelDonation(
|
||||
startDateStr = "2026-02-20",
|
||||
endDateStr = "2026-02-21",
|
||||
memberId = 7L,
|
||||
creatorNickname = "creator",
|
||||
offset = 10L,
|
||||
limit = 5L
|
||||
)
|
||||
}
|
||||
|
||||
private fun createMember(id: Long): Member {
|
||||
val member = Member(
|
||||
email = "creator@test.com",
|
||||
password = "password",
|
||||
nickname = "creator",
|
||||
role = MemberRole.CREATOR
|
||||
)
|
||||
member.id = id
|
||||
return member
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package kr.co.vividnext.sodalive.creator.admin.calculate.channelDonation
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
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.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
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 CreatorAdminChannelDonationCalculateQueryRepositoryTest @Autowired constructor(
|
||||
private val queryFactory: JPAQueryFactory,
|
||||
private val memberRepository: MemberRepository,
|
||||
private val useCanRepository: UseCanRepository,
|
||||
private val useCanCalculateRepository: UseCanCalculateRepository,
|
||||
private val entityManager: EntityManager
|
||||
) {
|
||||
private lateinit var repository: CreatorAdminChannelDonationCalculateQueryRepository
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
registerMysqlDateFunctions()
|
||||
repository = CreatorAdminChannelDonationCalculateQueryRepository(queryFactory)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("분할 정산 레코드가 있어도 크리에이터 정산 건수는 후원 단위로 집계한다")
|
||||
fun shouldCountDistinctUseCanForCreatorWhenDonationIsSplitAcrossCalculations() {
|
||||
val creator = saveMember(nickname = "creator-self", role = MemberRole.CREATOR)
|
||||
val sender = saveMember(nickname = "sender-self", role = MemberRole.USER)
|
||||
val useCan = saveUseCan(member = sender, can = 50, rewardCan = 0)
|
||||
|
||||
saveUseCanCalculate(
|
||||
useCan = useCan,
|
||||
recipientCreatorId = creator.id!!,
|
||||
can = 20,
|
||||
paymentGateway = PaymentGateway.PG
|
||||
)
|
||||
saveUseCanCalculate(
|
||||
useCan = useCan,
|
||||
recipientCreatorId = creator.id!!,
|
||||
can = 30,
|
||||
paymentGateway = PaymentGateway.GOOGLE_IAP
|
||||
)
|
||||
updateUseCanCreatedAt(useCan.id!!, LocalDateTime.of(2026, 2, 20, 12, 0, 0))
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
val startDate = LocalDateTime.of(2026, 2, 20, 0, 0, 0)
|
||||
val endDate = LocalDateTime.of(2026, 2, 20, 23, 59, 59)
|
||||
|
||||
val totalCount = repository.getChannelDonationTotalCount(startDate, endDate, creator.id!!)
|
||||
val items = repository.getChannelDonation(startDate, endDate, creator.id!!, offset = 0, limit = 20)
|
||||
|
||||
assertEquals(1, totalCount)
|
||||
assertEquals(1, items.size)
|
||||
assertEquals("2026-02-20", items[0].date)
|
||||
assertEquals(1L, items[0].count)
|
||||
assertEquals(50, items[0].totalCan)
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole): Member {
|
||||
return memberRepository.saveAndFlush(
|
||||
Member(
|
||||
email = "$nickname@test.com",
|
||||
password = "password",
|
||||
nickname = nickname,
|
||||
role = role
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveUseCan(member: Member, can: Int, rewardCan: Int): UseCan {
|
||||
val useCan = UseCan(
|
||||
canUsage = CanUsage.CHANNEL_DONATION,
|
||||
can = can,
|
||||
rewardCan = rewardCan
|
||||
)
|
||||
useCan.member = member
|
||||
return useCanRepository.saveAndFlush(useCan)
|
||||
}
|
||||
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package kr.co.vividnext.sodalive.creator.admin.calculate.channelDonation
|
||||
|
||||
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
|
||||
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
|
||||
|
||||
class CreatorAdminChannelDonationCalculateServiceTest {
|
||||
private lateinit var repository: CreatorAdminChannelDonationCalculateQueryRepository
|
||||
private lateinit var service: CreatorAdminChannelDonationCalculateService
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
repository = Mockito.mock(CreatorAdminChannelDonationCalculateQueryRepository::class.java)
|
||||
service = CreatorAdminChannelDonationCalculateService(repository)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("크리에이터 관리자 정산 조회는 본인 범위와 날짜 범위를 적용한다")
|
||||
fun shouldApplyMemberScopeAndDateRange() {
|
||||
val queryData = GetCreatorChannelDonationSettlementQueryData(
|
||||
date = "2026-02-26",
|
||||
count = 2L,
|
||||
totalCan = 50
|
||||
)
|
||||
|
||||
Mockito.`when`(
|
||||
repository.getChannelDonationTotalCount(
|
||||
"2026-02-20".convertLocalDateTime(),
|
||||
"2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
|
||||
7L
|
||||
)
|
||||
).thenReturn(1)
|
||||
Mockito.`when`(
|
||||
repository.getChannelDonation(
|
||||
"2026-02-20".convertLocalDateTime(),
|
||||
"2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
|
||||
7L,
|
||||
0L,
|
||||
20L
|
||||
)
|
||||
).thenReturn(listOf(queryData))
|
||||
|
||||
val result = service.getChannelDonation(
|
||||
startDateStr = "2026-02-20",
|
||||
endDateStr = "2026-02-21",
|
||||
memberId = 7L,
|
||||
creatorNickname = "creator-self",
|
||||
offset = 0,
|
||||
limit = 20
|
||||
)
|
||||
|
||||
assertEquals(1, result.totalCount)
|
||||
assertEquals(1, result.items.size)
|
||||
assertEquals("creator-self", result.items[0].creator)
|
||||
assertEquals(2, result.items[0].count)
|
||||
assertEquals(50, result.items[0].totalCan)
|
||||
assertEquals(5_000, result.items[0].krw)
|
||||
|
||||
Mockito.verify(repository).getChannelDonationTotalCount(
|
||||
"2026-02-20".convertLocalDateTime(),
|
||||
"2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
|
||||
7L
|
||||
)
|
||||
Mockito.verify(repository).getChannelDonation(
|
||||
"2026-02-20".convertLocalDateTime(),
|
||||
"2026-02-21".convertLocalDateTime(hour = 23, minute = 59, second = 59),
|
||||
7L,
|
||||
0L,
|
||||
20L
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package kr.co.vividnext.sodalive.support
|
||||
|
||||
import org.hibernate.dialect.H2Dialect
|
||||
import org.hibernate.dialect.function.StandardSQLFunction
|
||||
import org.hibernate.type.StandardBasicTypes
|
||||
|
||||
class H2MySqlFunctionDialect : H2Dialect() {
|
||||
init {
|
||||
registerFunction("date_format", StandardSQLFunction("DATE_FORMAT", StandardBasicTypes.STRING))
|
||||
registerFunction("DATE_FORMAT", StandardSQLFunction("DATE_FORMAT", StandardBasicTypes.STRING))
|
||||
registerFunction("convert_tz", StandardSQLFunction("CONVERT_TZ", StandardBasicTypes.TIMESTAMP))
|
||||
registerFunction("CONVERT_TZ", StandardSQLFunction("CONVERT_TZ", StandardBasicTypes.TIMESTAMP))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package kr.co.vividnext.sodalive.support
|
||||
|
||||
import java.sql.Timestamp
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
class H2MysqlDateFunctions {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun convertTz(value: Timestamp?, fromTz: String?, toTz: String?): Timestamp? {
|
||||
if (value == null || fromTz == null || toTz == null) {
|
||||
return value
|
||||
}
|
||||
|
||||
val fromZoneId = ZoneId.of(fromTz)
|
||||
val toZoneId = ZoneId.of(toTz)
|
||||
val converted = value.toLocalDateTime().atZone(fromZoneId).withZoneSameInstant(toZoneId).toLocalDateTime()
|
||||
|
||||
return Timestamp.valueOf(converted)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun dateFormat(value: Timestamp?, pattern: String?): String? {
|
||||
if (value == null || pattern == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val javaPattern = pattern
|
||||
.replace("%Y", "yyyy")
|
||||
.replace("%m", "MM")
|
||||
.replace("%d", "dd")
|
||||
|
||||
return value.toLocalDateTime().format(DateTimeFormatter.ofPattern(javaPattern))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user