feat(admin-calculate): 관리자 라이브 환불 처리와 정산 응답 식별자를 추가한다

This commit is contained in:
2026-03-16 12:25:50 +09:00
parent 02196eba4c
commit e2cbca1b84
10 changed files with 412 additions and 20 deletions

View File

@@ -7,6 +7,8 @@ import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@@ -18,6 +20,9 @@ import java.nio.charset.StandardCharsets
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/calculate")
class AdminCalculateController(private val service: AdminCalculateService) {
@PostMapping("/live/refund")
fun refundLive(@RequestBody request: AdminLiveRefundRequest) = ApiResponse.ok(service.refundLive(request))
@GetMapping("/live")
fun getCalculateLive(
@RequestParam startDateStr: String,

View File

@@ -50,10 +50,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
return queryFactory
.select(
QGetCalculateLiveQueryData(
member.email,
member.nickname,
formattedDate,
liveRoom.title,
liveRoom.id,
liveRoom.price,
useCan.canUsage,
useCan.id.count(),

View File

@@ -1,17 +1,102 @@
package kr.co.vividnext.sodalive.admin.calculate
import kr.co.vividnext.sodalive.can.CanRepository
import kr.co.vividnext.sodalive.can.charge.Charge
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
import kr.co.vividnext.sodalive.can.payment.Payment
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.can.use.CanUsage
import kr.co.vividnext.sodalive.can.use.UseCanCalculateRepository
import kr.co.vividnext.sodalive.can.use.UseCanCalculateStatus
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.creator.admin.calculate.GetCreatorCalculateCommunityPostResponse
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.live.room.LiveRoomRepository
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.xssf.streaming.SXSSFWorkbook
import org.springframework.cache.annotation.Cacheable
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody
import java.time.LocalDateTime
@Service
class AdminCalculateService(private val repository: AdminCalculateQueryRepository) {
class AdminCalculateService(
private val repository: AdminCalculateQueryRepository,
private val canRepository: CanRepository,
private val useCanCalculateRepository: UseCanCalculateRepository,
private val chargeRepository: ChargeRepository,
private val liveRoomRepository: LiveRoomRepository,
private val messageSource: SodaMessageSource,
private val langContext: LangContext
) {
private fun formatMessage(key: String, vararg args: Any): String {
val template = messageSource.getMessage(key, langContext.lang).orEmpty()
return if (args.isNotEmpty()) {
String.format(template, *args)
} else {
template
}
}
@Transactional
fun refundLive(request: AdminLiveRefundRequest) {
if (request.roomId == null || request.canUsageStr.isNullOrBlank()) {
throw SodaException(messageKey = "common.error.invalid_request")
}
val room = liveRoomRepository.findByIdOrNull(request.roomId)
?: throw SodaException(messageKey = "live.room.not_found")
val canUsage = when (request.canUsageStr) {
"유료" -> CanUsage.LIVE
"룰렛" -> CanUsage.SPIN_ROULETTE
"하트" -> CanUsage.HEART
"후원" -> CanUsage.DONATION
else -> throw SodaException(message = "Invalid canUsageStr: ${request.canUsageStr}")
}
val useCanList = canRepository.findAllByRoomIdAndCanUsageAndIsRefundFalse(
roomId = room.id!!,
canUsage = canUsage
)
for (useCan in useCanList) {
useCan.isRefund = true
val member = useCan.member!!
val useCanCalculate = useCanCalculateRepository.findByUseCanIdAndStatus(useCanId = useCan.id!!)
useCanCalculate.forEach {
it.status = UseCanCalculateStatus.REFUND
val charge = Charge(0, it.can, status = ChargeStatus.REFUND_CHARGE)
charge.title = formatMessage("live.room.can_title", it.can)
charge.useCan = useCan
when (it.paymentGateway) {
PaymentGateway.GOOGLE_IAP -> member.googleRewardCan += charge.rewardCan
PaymentGateway.APPLE_IAP -> member.appleRewardCan += charge.rewardCan
else -> member.pgRewardCan += charge.rewardCan
}
charge.member = member
val payment = Payment(
status = PaymentStatus.COMPLETE,
paymentGateway = it.paymentGateway
)
payment.method = formatMessage("live.room.refund_method")
charge.payment = payment
chargeRepository.save(charge)
}
}
}
@Transactional(readOnly = true)
fun getCalculateLive(
startDateStr: String,
@@ -164,7 +249,6 @@ class AdminCalculateService(private val repository: AdminCalculateQueryRepositor
return createExcelStream(
sheetName = "라이브 정산",
headers = listOf(
"이메일",
"닉네임",
"날짜",
"라이브 제목",
@@ -181,19 +265,18 @@ class AdminCalculateService(private val repository: AdminCalculateQueryRepositor
) { sheet ->
items.forEachIndexed { index, item ->
val row = sheet.createRow(index + 1)
row.createCell(0).setCellValue(item.email)
row.createCell(1).setCellValue(item.nickname)
row.createCell(2).setCellValue(item.date)
row.createCell(3).setCellValue(item.title)
row.createCell(4).setCellValue(item.entranceFee.toDouble())
row.createCell(5).setCellValue(item.canUsageStr)
row.createCell(6).setCellValue(item.numberOfPeople.toDouble())
row.createCell(7).setCellValue(item.totalAmount.toDouble())
row.createCell(8).setCellValue(item.totalKrw.toDouble())
row.createCell(9).setCellValue(item.paymentFee.toDouble())
row.createCell(10).setCellValue(item.settlementAmount.toDouble())
row.createCell(11).setCellValue(item.tax.toDouble())
row.createCell(12).setCellValue(item.depositAmount.toDouble())
row.createCell(0).setCellValue(item.nickname)
row.createCell(1).setCellValue(item.date)
row.createCell(2).setCellValue(item.title)
row.createCell(3).setCellValue(item.entranceFee.toDouble())
row.createCell(4).setCellValue(item.canUsageStr)
row.createCell(5).setCellValue(item.numberOfPeople.toDouble())
row.createCell(6).setCellValue(item.totalAmount.toDouble())
row.createCell(7).setCellValue(item.totalKrw.toDouble())
row.createCell(8).setCellValue(item.paymentFee.toDouble())
row.createCell(9).setCellValue(item.settlementAmount.toDouble())
row.createCell(10).setCellValue(item.tax.toDouble())
row.createCell(11).setCellValue(item.depositAmount.toDouble())
}
}
}

View File

@@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.admin.calculate
import com.fasterxml.jackson.annotation.JsonProperty
data class AdminLiveRefundRequest(
@JsonProperty("roomId") val roomId: Long?,
@JsonProperty("canUsageStr") val canUsageStr: String?
)

View File

@@ -6,10 +6,11 @@ import java.math.BigDecimal
import java.math.RoundingMode
data class GetCalculateLiveQueryData @QueryProjection constructor(
val email: String,
val nickname: String,
val date: String,
val title: String,
// 라이브 방 id
val roomId: Long,
// 유료방 입장 금액
val entranceFee: Int,
// 코인 사용 구분
@@ -66,10 +67,10 @@ data class GetCalculateLiveQueryData @QueryProjection constructor(
val depositAmount = settlementAmount.subtract(tax)
return GetCalculateLiveResponse(
email = email,
nickname = nickname,
date = date,
title = title,
roomId = roomId,
entranceFee = entranceFee,
canUsageStr = canUsageStr,
numberOfPeople = numberOfPeople,

View File

@@ -3,10 +3,10 @@ package kr.co.vividnext.sodalive.admin.calculate
import com.fasterxml.jackson.annotation.JsonProperty
data class GetCalculateLiveResponse(
@JsonProperty("email") val email: String,
@JsonProperty("nickname") val nickname: String,
@JsonProperty("date") val date: String,
@JsonProperty("title") val title: String,
@JsonProperty("roomId") val roomId: Long,
@JsonProperty("entranceFee") val entranceFee: Int,
@JsonProperty("canUsageStr") val canUsageStr: String,
@JsonProperty("numberOfPeople") val numberOfPeople: Int,

View File

@@ -28,6 +28,7 @@ interface CanQueryRepository {
fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List<Charge>
fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan?
fun getCanUsedForLiveRoomNotRefund(memberId: Long, roomId: Long, canUsage: CanUsage = CanUsage.LIVE): UseCan?
fun findAllByRoomIdAndCanUsageAndIsRefundFalse(roomId: Long, canUsage: CanUsage): List<UseCan>
}
@Repository
@@ -139,4 +140,16 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue
.orderBy(useCan.id.desc())
.fetchFirst()
}
override fun findAllByRoomIdAndCanUsageAndIsRefundFalse(roomId: Long, canUsage: CanUsage): List<UseCan> {
return queryFactory
.selectFrom(useCan)
.innerJoin(useCan.room, liveRoom)
.where(
liveRoom.id.eq(roomId)
.and(useCan.canUsage.eq(canUsage))
.and(useCan.isRefund.isFalse)
)
.fetch()
}
}

View File

@@ -38,10 +38,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
return queryFactory
.select(
QGetCalculateLiveQueryData(
member.email,
member.nickname,
formattedDate,
liveRoom.title,
liveRoom.id,
liveRoom.price,
useCan.canUsage,
useCan.id.count(),