Merge pull request 'payverse 적용' (#344) from test into main
Reviewed-on: #344
This commit is contained in:
@@ -39,7 +39,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
||||
.innerJoin(useCan.room, liveRoom)
|
||||
.innerJoin(liveRoom.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
useCan.isRefund.isFalse
|
||||
.and(useCan.createdAt.goe(startDate))
|
||||
@@ -75,7 +78,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
||||
.innerJoin(order.audioContent, audioContent)
|
||||
.innerJoin(audioContent.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
order.createdAt.goe(startDate)
|
||||
.and(order.createdAt.loe(endDate))
|
||||
@@ -142,7 +148,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
||||
.innerJoin(order.audioContent, audioContent)
|
||||
.innerJoin(audioContent.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(order.isActive.isTrue)
|
||||
.groupBy(
|
||||
member.id,
|
||||
@@ -230,7 +239,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
||||
.innerJoin(useCan.communityPost, creatorCommunity)
|
||||
.innerJoin(creatorCommunity.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
useCan.isRefund.isFalse
|
||||
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
|
||||
@@ -251,7 +263,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
||||
.innerJoin(useCan.room, liveRoom)
|
||||
.innerJoin(liveRoom.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
useCan.isRefund.isFalse
|
||||
.and(useCan.createdAt.goe(startDate))
|
||||
@@ -281,7 +296,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
||||
.innerJoin(useCan.room, liveRoom)
|
||||
.innerJoin(liveRoom.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
useCan.isRefund.isFalse
|
||||
.and(useCan.createdAt.goe(startDate))
|
||||
@@ -301,7 +319,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
||||
.innerJoin(order.audioContent, audioContent)
|
||||
.innerJoin(audioContent.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
order.createdAt.goe(startDate)
|
||||
.and(order.createdAt.loe(endDate))
|
||||
@@ -331,7 +352,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
||||
.innerJoin(order.audioContent, audioContent)
|
||||
.innerJoin(audioContent.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
order.createdAt.goe(startDate)
|
||||
.and(order.createdAt.loe(endDate))
|
||||
@@ -351,7 +375,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
||||
.innerJoin(useCan.communityPost, creatorCommunity)
|
||||
.innerJoin(creatorCommunity.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
useCan.isRefund.isFalse
|
||||
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
|
||||
@@ -382,7 +409,10 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) {
|
||||
.innerJoin(useCan.communityPost, creatorCommunity)
|
||||
.innerJoin(creatorCommunity.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
useCan.isRefund.isFalse
|
||||
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
|
||||
|
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.admin.calculate.ratio
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.FetchType
|
||||
import javax.persistence.JoinColumn
|
||||
@@ -9,12 +10,29 @@ import javax.persistence.OneToOne
|
||||
|
||||
@Entity
|
||||
data class CreatorSettlementRatio(
|
||||
val subsidy: Int,
|
||||
val liveSettlementRatio: Int,
|
||||
val contentSettlementRatio: Int,
|
||||
val communitySettlementRatio: Int
|
||||
var subsidy: Int,
|
||||
var liveSettlementRatio: Int,
|
||||
var contentSettlementRatio: Int,
|
||||
var communitySettlementRatio: Int
|
||||
) : BaseEntity() {
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id", nullable = false)
|
||||
var member: Member? = null
|
||||
|
||||
var deletedAt: LocalDateTime? = null
|
||||
|
||||
fun softDelete() {
|
||||
this.deletedAt = LocalDateTime.now()
|
||||
}
|
||||
|
||||
fun restore() {
|
||||
this.deletedAt = null
|
||||
}
|
||||
|
||||
fun updateValues(subsidy: Int, live: Int, content: Int, community: Int) {
|
||||
this.subsidy = subsidy
|
||||
this.liveSettlementRatio = live
|
||||
this.contentSettlementRatio = content
|
||||
this.communitySettlementRatio = community
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ 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.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
@@ -27,4 +28,14 @@ class CreatorSettlementRatioController(private val service: CreatorSettlementRat
|
||||
limit = pageable.pageSize.toLong()
|
||||
)
|
||||
)
|
||||
|
||||
@PostMapping("/update")
|
||||
fun updateCreatorSettlementRatio(
|
||||
@RequestBody request: CreateCreatorSettlementRatioRequest
|
||||
) = ApiResponse.ok(service.updateCreatorSettlementRatio(request))
|
||||
|
||||
@PostMapping("/delete/{memberId}")
|
||||
fun deleteCreatorSettlementRatio(
|
||||
@PathVariable memberId: Long
|
||||
) = ApiResponse.ok(service.deleteCreatorSettlementRatio(memberId))
|
||||
}
|
||||
|
@@ -7,7 +7,9 @@ import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface CreatorSettlementRatioRepository :
|
||||
JpaRepository<CreatorSettlementRatio, Long>,
|
||||
CreatorSettlementRatioQueryRepository
|
||||
CreatorSettlementRatioQueryRepository {
|
||||
fun findByMemberId(memberId: Long): CreatorSettlementRatio?
|
||||
}
|
||||
|
||||
interface CreatorSettlementRatioQueryRepository {
|
||||
fun getCreatorSettlementRatio(offset: Long, limit: Long): List<GetCreatorSettlementRatioItem>
|
||||
@@ -21,6 +23,7 @@ class CreatorSettlementRatioQueryRepositoryImpl(
|
||||
return queryFactory
|
||||
.select(
|
||||
QGetCreatorSettlementRatioItem(
|
||||
member.id,
|
||||
member.nickname,
|
||||
creatorSettlementRatio.subsidy,
|
||||
creatorSettlementRatio.liveSettlementRatio,
|
||||
@@ -30,6 +33,7 @@ class CreatorSettlementRatioQueryRepositoryImpl(
|
||||
)
|
||||
.from(creatorSettlementRatio)
|
||||
.innerJoin(creatorSettlementRatio.member, member)
|
||||
.where(creatorSettlementRatio.deletedAt.isNull)
|
||||
.orderBy(creatorSettlementRatio.id.asc())
|
||||
.offset(offset)
|
||||
.limit(limit)
|
||||
@@ -40,6 +44,7 @@ class CreatorSettlementRatioQueryRepositoryImpl(
|
||||
return queryFactory
|
||||
.select(creatorSettlementRatio.id)
|
||||
.from(creatorSettlementRatio)
|
||||
.where(creatorSettlementRatio.deletedAt.isNull)
|
||||
.fetch()
|
||||
.size
|
||||
}
|
||||
|
@@ -14,8 +14,6 @@ class CreatorSettlementRatioService(
|
||||
) {
|
||||
@Transactional
|
||||
fun createCreatorSettlementRatio(request: CreateCreatorSettlementRatioRequest) {
|
||||
val creatorSettlementRatio = request.toEntity()
|
||||
|
||||
val creator = memberRepository.findByIdOrNull(request.memberId)
|
||||
?: throw SodaException("잘못된 크리에이터 입니다.")
|
||||
|
||||
@@ -23,10 +21,52 @@ class CreatorSettlementRatioService(
|
||||
throw SodaException("잘못된 크리에이터 입니다.")
|
||||
}
|
||||
|
||||
val existing = repository.findByMemberId(request.memberId)
|
||||
if (existing != null) {
|
||||
// revive if soft-deleted, then update values
|
||||
existing.restore()
|
||||
existing.updateValues(
|
||||
request.subsidy,
|
||||
request.liveSettlementRatio,
|
||||
request.contentSettlementRatio,
|
||||
request.communitySettlementRatio
|
||||
)
|
||||
repository.save(existing)
|
||||
return
|
||||
}
|
||||
|
||||
val creatorSettlementRatio = request.toEntity()
|
||||
creatorSettlementRatio.member = creator
|
||||
repository.save(creatorSettlementRatio)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun updateCreatorSettlementRatio(request: CreateCreatorSettlementRatioRequest) {
|
||||
val creator = memberRepository.findByIdOrNull(request.memberId)
|
||||
?: throw SodaException("잘못된 크리에이터 입니다.")
|
||||
if (creator.role != MemberRole.CREATOR) {
|
||||
throw SodaException("잘못된 크리에이터 입니다.")
|
||||
}
|
||||
val existing = repository.findByMemberId(request.memberId)
|
||||
?: throw SodaException("해당 크리에이터의 정산 비율 설정이 없습니다.")
|
||||
existing.restore()
|
||||
existing.updateValues(
|
||||
request.subsidy,
|
||||
request.liveSettlementRatio,
|
||||
request.contentSettlementRatio,
|
||||
request.communitySettlementRatio
|
||||
)
|
||||
repository.save(existing)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteCreatorSettlementRatio(memberId: Long) {
|
||||
val existing = repository.findByMemberId(memberId)
|
||||
?: throw SodaException("해당 크리에이터의 정산 비율 설정이 없습니다.")
|
||||
existing.softDelete()
|
||||
repository.save(existing)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getCreatorSettlementRatio(offset: Long, limit: Long): GetCreatorSettlementRatioResponse {
|
||||
val totalCount = repository.getCreatorSettlementRatioTotalCount()
|
||||
|
@@ -8,6 +8,7 @@ data class GetCreatorSettlementRatioResponse(
|
||||
)
|
||||
|
||||
data class GetCreatorSettlementRatioItem @QueryProjection constructor(
|
||||
val memberId: Long,
|
||||
val nickname: String,
|
||||
val subsidy: Int,
|
||||
val liveSettlementRatio: Int,
|
||||
|
@@ -1,8 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.admin.can
|
||||
|
||||
import kr.co.vividnext.sodalive.can.CanResponse
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.DeleteMapping
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
@@ -13,6 +15,11 @@ import org.springframework.web.bind.annotation.RestController
|
||||
@RequestMapping("/admin/can")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
class AdminCanController(private val service: AdminCanService) {
|
||||
@GetMapping
|
||||
fun getCans(): ApiResponse<List<CanResponse>> {
|
||||
return ApiResponse.ok(service.getCans())
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
fun insertCan(@RequestBody request: AdminCanRequest) = ApiResponse.ok(service.saveCan(request))
|
||||
|
||||
|
@@ -1,6 +1,38 @@
|
||||
package kr.co.vividnext.sodalive.admin.can
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.can.Can
|
||||
import kr.co.vividnext.sodalive.can.CanResponse
|
||||
import kr.co.vividnext.sodalive.can.CanStatus
|
||||
import kr.co.vividnext.sodalive.can.QCan.can1
|
||||
import kr.co.vividnext.sodalive.can.QCanResponse
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
interface AdminCanRepository : JpaRepository<Can, Long>
|
||||
interface AdminCanRepository : JpaRepository<Can, Long>, AdminCanQueryRepository
|
||||
|
||||
interface AdminCanQueryRepository {
|
||||
fun findAllByStatus(status: CanStatus): List<CanResponse>
|
||||
}
|
||||
|
||||
@Repository
|
||||
class AdminCanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminCanQueryRepository {
|
||||
override fun findAllByStatus(status: CanStatus): List<CanResponse> {
|
||||
return queryFactory
|
||||
.select(
|
||||
QCanResponse(
|
||||
can1.id,
|
||||
can1.title,
|
||||
can1.can,
|
||||
can1.rewardCan,
|
||||
can1.price.intValue(),
|
||||
can1.currency,
|
||||
can1.price.stringValue()
|
||||
)
|
||||
)
|
||||
.from(can1)
|
||||
.where(can1.status.eq(status))
|
||||
.orderBy(can1.currency.asc(), can1.price.asc())
|
||||
.fetch()
|
||||
}
|
||||
}
|
||||
|
@@ -3,11 +3,13 @@ package kr.co.vividnext.sodalive.admin.can
|
||||
import kr.co.vividnext.sodalive.can.Can
|
||||
import kr.co.vividnext.sodalive.can.CanStatus
|
||||
import kr.co.vividnext.sodalive.extensions.moneyFormat
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class AdminCanRequest(
|
||||
val can: Int,
|
||||
val rewardCan: Int,
|
||||
val price: Int
|
||||
val price: BigDecimal,
|
||||
val currency: String
|
||||
) {
|
||||
fun toEntity(): Can {
|
||||
var title = "${can.moneyFormat()} 캔"
|
||||
@@ -20,6 +22,7 @@ data class AdminCanRequest(
|
||||
can = can,
|
||||
rewardCan = rewardCan,
|
||||
price = price,
|
||||
currency = currency,
|
||||
status = CanStatus.SALE
|
||||
)
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.admin.can
|
||||
|
||||
import kr.co.vividnext.sodalive.admin.member.AdminMemberRepository
|
||||
import kr.co.vividnext.sodalive.can.CanResponse
|
||||
import kr.co.vividnext.sodalive.can.CanStatus
|
||||
import kr.co.vividnext.sodalive.can.charge.Charge
|
||||
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
|
||||
@@ -20,6 +21,10 @@ class AdminCanService(
|
||||
private val chargeRepository: ChargeRepository,
|
||||
private val memberRepository: AdminMemberRepository
|
||||
) {
|
||||
fun getCans(): List<CanResponse> {
|
||||
return repository.findAllByStatus(status = CanStatus.SALE)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun saveCan(request: AdminCanRequest) {
|
||||
repository.save(request.toEntity())
|
||||
|
@@ -20,16 +20,15 @@ class AdminChargeStatusService(val repository: AdminChargeStatusQueryRepository)
|
||||
.withZoneSameInstant(ZoneId.of("UTC"))
|
||||
.toLocalDateTime()
|
||||
|
||||
var totalChargeAmount = 0
|
||||
var totalChargeAmount = 0.toBigDecimal()
|
||||
var totalChargeCount = 0L
|
||||
|
||||
val chargeStatusList = repository.getChargeStatus(startDate, endDate)
|
||||
.asSequence()
|
||||
.map {
|
||||
val chargeAmount = if (it.paymentGateWay == PaymentGateway.PG) {
|
||||
it.pgChargeAmount
|
||||
} else {
|
||||
it.appleChargeAmount.toInt()
|
||||
it.appleChargeAmount
|
||||
}
|
||||
|
||||
val chargeCount = it.chargeCount
|
||||
|
@@ -1,13 +1,14 @@
|
||||
package kr.co.vividnext.sodalive.admin.charge
|
||||
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class GetChargeStatusDetailQueryDto @QueryProjection constructor(
|
||||
val memberId: Long,
|
||||
val nickname: String,
|
||||
val method: String,
|
||||
val appleChargeAmount: Double,
|
||||
val pgChargeAmount: Int,
|
||||
val appleChargeAmount: BigDecimal,
|
||||
val pgChargeAmount: BigDecimal,
|
||||
val locale: String,
|
||||
val datetime: String
|
||||
)
|
||||
|
@@ -2,11 +2,12 @@ package kr.co.vividnext.sodalive.admin.charge
|
||||
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class GetChargeStatusQueryDto @QueryProjection constructor(
|
||||
val date: String,
|
||||
val appleChargeAmount: Double,
|
||||
val pgChargeAmount: Int,
|
||||
val appleChargeAmount: BigDecimal,
|
||||
val pgChargeAmount: BigDecimal,
|
||||
val chargeCount: Long,
|
||||
val paymentGateWay: PaymentGateway
|
||||
)
|
||||
|
@@ -1,8 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.admin.charge
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class GetChargeStatusResponse(
|
||||
val date: String,
|
||||
val chargeAmount: Int,
|
||||
val chargeAmount: BigDecimal,
|
||||
val chargeCount: Long,
|
||||
val pg: String
|
||||
)
|
||||
|
@@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.admin.statistics.ad
|
||||
import com.querydsl.core.types.dsl.CaseBuilder
|
||||
import com.querydsl.core.types.dsl.DateTimePath
|
||||
import com.querydsl.core.types.dsl.Expressions
|
||||
import com.querydsl.core.types.dsl.NumberExpression
|
||||
import com.querydsl.core.types.dsl.StringTemplate
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.marketing.AdTrackingHistoryType
|
||||
@@ -67,7 +66,7 @@ class AdminAdStatisticsRepository(private val queryFactory: JPAQueryFactory) {
|
||||
val firstPaymentTotalAmount = CaseBuilder()
|
||||
.`when`(adTrackingHistory.type.eq(AdTrackingHistoryType.FIRST_PAYMENT))
|
||||
.then(adTrackingHistory.price)
|
||||
.otherwise(Expressions.constant(0.0))
|
||||
.otherwise(0.toBigDecimal())
|
||||
.sum()
|
||||
|
||||
val repeatPaymentCount = CaseBuilder()
|
||||
@@ -79,7 +78,7 @@ class AdminAdStatisticsRepository(private val queryFactory: JPAQueryFactory) {
|
||||
val repeatPaymentTotalAmount = CaseBuilder()
|
||||
.`when`(adTrackingHistory.type.eq(AdTrackingHistoryType.REPEAT_PAYMENT))
|
||||
.then(adTrackingHistory.price)
|
||||
.otherwise(Expressions.constant(0.0))
|
||||
.otherwise(0.toBigDecimal())
|
||||
.sum()
|
||||
|
||||
val allPaymentCount = CaseBuilder()
|
||||
@@ -97,7 +96,7 @@ class AdminAdStatisticsRepository(private val queryFactory: JPAQueryFactory) {
|
||||
.or(adTrackingHistory.type.eq(AdTrackingHistoryType.REPEAT_PAYMENT))
|
||||
)
|
||||
.then(adTrackingHistory.price)
|
||||
.otherwise(Expressions.constant(0.0))
|
||||
.otherwise(0.toBigDecimal())
|
||||
.sum()
|
||||
|
||||
return queryFactory
|
||||
@@ -111,11 +110,11 @@ class AdminAdStatisticsRepository(private val queryFactory: JPAQueryFactory) {
|
||||
loginCount,
|
||||
signUpCount,
|
||||
firstPaymentCount,
|
||||
roundedValueDecimalPlaces2(firstPaymentTotalAmount),
|
||||
firstPaymentTotalAmount,
|
||||
repeatPaymentCount,
|
||||
roundedValueDecimalPlaces2(repeatPaymentTotalAmount),
|
||||
repeatPaymentTotalAmount,
|
||||
allPaymentCount,
|
||||
roundedValueDecimalPlaces2(allPaymentTotalAmount)
|
||||
allPaymentTotalAmount
|
||||
)
|
||||
)
|
||||
.from(adTrackingHistory)
|
||||
@@ -148,13 +147,4 @@ class AdminAdStatisticsRepository(private val queryFactory: JPAQueryFactory) {
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
}
|
||||
|
||||
private fun roundedValueDecimalPlaces2(valueExpression: NumberExpression<Double>): NumberExpression<Double> {
|
||||
return Expressions.numberTemplate(
|
||||
Double::class.java,
|
||||
"ROUND({0}, {1})",
|
||||
valueExpression,
|
||||
2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.admin.statistics.ad
|
||||
|
||||
import com.querydsl.core.annotations.QueryProjection
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class GetAdminAdStatisticsResponse(
|
||||
val totalCount: Int,
|
||||
@@ -16,9 +17,9 @@ data class GetAdminAdStatisticsItem @QueryProjection constructor(
|
||||
val loginCount: Int,
|
||||
val signUpCount: Int,
|
||||
val firstPaymentCount: Int,
|
||||
val firstPaymentTotalAmount: Double,
|
||||
val firstPaymentTotalAmount: BigDecimal,
|
||||
val repeatPaymentCount: Int,
|
||||
val repeatPaymentTotalAmount: Double,
|
||||
val repeatPaymentTotalAmount: BigDecimal,
|
||||
val allPaymentCount: Int,
|
||||
val allPaymentTotalAmount: Double
|
||||
val allPaymentTotalAmount: BigDecimal
|
||||
)
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package kr.co.vividnext.sodalive.can
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import java.math.BigDecimal
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.EnumType
|
||||
import javax.persistence.Enumerated
|
||||
@@ -10,7 +12,10 @@ data class Can(
|
||||
var title: String,
|
||||
var can: Int,
|
||||
var rewardCan: Int,
|
||||
var price: Int,
|
||||
@Column(precision = 10, scale = 4, nullable = false)
|
||||
var price: BigDecimal,
|
||||
@Column(length = 3, nullable = false, columnDefinition = "CHAR(3)")
|
||||
var currency: String,
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
var status: CanStatus
|
||||
) : BaseEntity()
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package kr.co.vividnext.sodalive.can
|
||||
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.GeoCountry
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.data.domain.Pageable
|
||||
@@ -9,13 +10,15 @@ 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
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/can")
|
||||
class CanController(private val service: CanService) {
|
||||
@GetMapping
|
||||
fun getCans(): ApiResponse<List<CanResponse>> {
|
||||
return ApiResponse.ok(service.getCans())
|
||||
fun getCans(request: HttpServletRequest): ApiResponse<List<CanResponse>> {
|
||||
val geoCountry = request.getAttribute("geoCountry") as? GeoCountry ?: GeoCountry.OTHER
|
||||
return ApiResponse.ok(service.getCans(geoCountry))
|
||||
}
|
||||
|
||||
@GetMapping("/status")
|
||||
|
@@ -23,7 +23,7 @@ import org.springframework.stereotype.Repository
|
||||
interface CanRepository : JpaRepository<Can, Long>, CanQueryRepository
|
||||
|
||||
interface CanQueryRepository {
|
||||
fun findAllByStatus(status: CanStatus): List<CanResponse>
|
||||
fun findAllByStatusAndCurrency(status: CanStatus, currency: String): List<CanResponse>
|
||||
fun getCanUseStatus(member: Member, pageable: Pageable): List<UseCan>
|
||||
fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List<Charge>
|
||||
fun isExistPaidLiveRoom(memberId: Long, roomId: Long): UseCan?
|
||||
@@ -32,7 +32,7 @@ interface CanQueryRepository {
|
||||
|
||||
@Repository
|
||||
class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQueryRepository {
|
||||
override fun findAllByStatus(status: CanStatus): List<CanResponse> {
|
||||
override fun findAllByStatusAndCurrency(status: CanStatus, currency: String): List<CanResponse> {
|
||||
return queryFactory
|
||||
.select(
|
||||
QCanResponse(
|
||||
@@ -40,11 +40,16 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue
|
||||
can1.title,
|
||||
can1.can,
|
||||
can1.rewardCan,
|
||||
can1.price
|
||||
can1.price.intValue(),
|
||||
can1.currency,
|
||||
can1.price.stringValue()
|
||||
)
|
||||
)
|
||||
.from(can1)
|
||||
.where(can1.status.eq(status))
|
||||
.where(
|
||||
can1.status.eq(status),
|
||||
can1.currency.eq(currency)
|
||||
)
|
||||
.orderBy(can1.can.asc())
|
||||
.fetch()
|
||||
}
|
||||
@@ -64,11 +69,13 @@ class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQue
|
||||
val chargeStatusCondition = when (container) {
|
||||
"aos" -> {
|
||||
charge.payment.paymentGateway.eq(PaymentGateway.PG)
|
||||
.or(charge.payment.paymentGateway.eq(PaymentGateway.PAYVERSE))
|
||||
.or(charge.payment.paymentGateway.eq(PaymentGateway.GOOGLE_IAP))
|
||||
}
|
||||
|
||||
"ios" -> {
|
||||
charge.payment.paymentGateway.eq(PaymentGateway.PG)
|
||||
.or(charge.payment.paymentGateway.eq(PaymentGateway.PAYVERSE))
|
||||
.or(charge.payment.paymentGateway.eq(PaymentGateway.APPLE_IAP))
|
||||
}
|
||||
|
||||
|
@@ -7,5 +7,7 @@ data class CanResponse @QueryProjection constructor(
|
||||
val title: String,
|
||||
val can: Int,
|
||||
val rewardCan: Int,
|
||||
val price: Int
|
||||
val price: Int,
|
||||
val currency: String,
|
||||
val priceStr: String
|
||||
)
|
||||
|
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.can
|
||||
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
|
||||
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||
import kr.co.vividnext.sodalive.can.use.CanUsage
|
||||
import kr.co.vividnext.sodalive.common.GeoCountry
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -11,8 +12,12 @@ import java.time.format.DateTimeFormatter
|
||||
|
||||
@Service
|
||||
class CanService(private val repository: CanRepository) {
|
||||
fun getCans(): List<CanResponse> {
|
||||
return repository.findAllByStatus(status = CanStatus.SALE)
|
||||
fun getCans(geoCountry: GeoCountry): List<CanResponse> {
|
||||
val currency = when (geoCountry) {
|
||||
GeoCountry.KR -> "KRW"
|
||||
else -> "USD"
|
||||
}
|
||||
return repository.findAllByStatusAndCurrency(status = CanStatus.SALE, currency = currency)
|
||||
}
|
||||
|
||||
fun getCanStatus(member: Member, container: String): GetCanStatusResponse {
|
||||
|
@@ -1,7 +1,9 @@
|
||||
package kr.co.vividnext.sodalive.can.charge
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class ChargeCompleteResponse(
|
||||
val price: Double,
|
||||
val price: BigDecimal,
|
||||
val currencyCode: String,
|
||||
val isFirstCharged: Boolean
|
||||
)
|
||||
|
@@ -6,20 +6,77 @@ import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.marketing.AdTrackingHistoryType
|
||||
import kr.co.vividnext.sodalive.marketing.AdTrackingService
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
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.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.time.LocalDateTime
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/charge")
|
||||
class ChargeController(
|
||||
private val service: ChargeService,
|
||||
private val trackingService: AdTrackingService
|
||||
private val trackingService: AdTrackingService,
|
||||
|
||||
@Value("\${payverse.inbound-ip}")
|
||||
private val payverseInboundIp: String
|
||||
) {
|
||||
|
||||
@PostMapping("/payverse")
|
||||
fun payverseCharge(
|
||||
@RequestBody request: PayverseChargeRequest,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) {
|
||||
throw SodaException("로그인 정보를 확인해주세요.")
|
||||
}
|
||||
|
||||
ApiResponse.ok(service.payverseCharge(member, request))
|
||||
}
|
||||
|
||||
@PostMapping("/payverse/verify")
|
||||
fun payverseVerify(
|
||||
@RequestBody verifyRequest: PayverseVerifyRequest,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) {
|
||||
throw SodaException("로그인 정보를 확인해주세요.")
|
||||
}
|
||||
|
||||
val response = service.payverseVerify(memberId = member.id!!, verifyRequest)
|
||||
trackingCharge(member, response)
|
||||
ApiResponse.ok(Unit)
|
||||
}
|
||||
|
||||
// Payverse Webhook 엔드포인트 (payverseVerify 아래)
|
||||
@PostMapping("/payverse/webhook")
|
||||
fun payverseWebhook(
|
||||
@RequestBody request: PayverseWebhookRequest,
|
||||
servletRequest: HttpServletRequest
|
||||
): PayverseWebhookResponse {
|
||||
val header = servletRequest.getHeader("X-Forwarded-For")
|
||||
val remoteIp = if (header.isNullOrEmpty()) {
|
||||
servletRequest.remoteAddr
|
||||
} else {
|
||||
header.split(",")[0].trim() // 첫 번째 값이 클라이언트 IP
|
||||
}
|
||||
|
||||
if (remoteIp != payverseInboundIp) {
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
val success = service.payverseWebhook(request)
|
||||
if (!success) {
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
return PayverseWebhookResponse(receiveResult = "SUCCESS")
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
fun charge(
|
||||
@RequestBody chargeRequest: ChargeRequest,
|
||||
@@ -111,8 +168,7 @@ class ChargeController(
|
||||
memberId = member.id!!,
|
||||
chargeId = chargeId,
|
||||
productId = request.productId,
|
||||
purchaseToken = request.purchaseToken,
|
||||
paymentGateway = request.paymentGateway
|
||||
purchaseToken = request.purchaseToken
|
||||
)
|
||||
|
||||
trackingCharge(member, response)
|
||||
|
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.can.charge
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class ChargeRequest(val canId: Long, val paymentGateway: PaymentGateway)
|
||||
|
||||
@@ -20,14 +21,14 @@ data class VerifyResult(
|
||||
val method: String,
|
||||
val pg: String,
|
||||
val status: Int,
|
||||
val price: Int
|
||||
val price: BigDecimal
|
||||
)
|
||||
|
||||
data class AppleChargeRequest(
|
||||
val title: String,
|
||||
val chargeCan: Int,
|
||||
val paymentGateway: PaymentGateway,
|
||||
var price: Double? = null,
|
||||
var price: BigDecimal? = null,
|
||||
var locale: String? = null
|
||||
)
|
||||
|
||||
@@ -38,9 +39,53 @@ data class AppleVerifyResponse(val status: Int)
|
||||
data class GoogleChargeRequest(
|
||||
val title: String,
|
||||
val chargeCan: Int,
|
||||
val price: Double,
|
||||
val price: BigDecimal,
|
||||
val currencyCode: String,
|
||||
val productId: String,
|
||||
val purchaseToken: String,
|
||||
val paymentGateway: PaymentGateway
|
||||
)
|
||||
|
||||
data class PayverseChargeRequest(
|
||||
val canId: Long
|
||||
)
|
||||
|
||||
data class PayverseChargeResponse(
|
||||
val chargeId: Long,
|
||||
val payloadJson: String
|
||||
)
|
||||
|
||||
data class PayverseVerifyRequest(
|
||||
val transactionId: String,
|
||||
val orderId: String
|
||||
)
|
||||
|
||||
data class PayverseVerifyResponse(
|
||||
val resultStatus: String,
|
||||
val tid: String,
|
||||
val schemeGroup: String,
|
||||
val schemeCode: String,
|
||||
val transactionType: String,
|
||||
val transactionStatus: String,
|
||||
val transactionMessage: String,
|
||||
val orderId: String,
|
||||
val customerId: String,
|
||||
val requestCurrency: String,
|
||||
val requestAmount: BigDecimal
|
||||
)
|
||||
|
||||
data class PayverseWebhookRequest(
|
||||
val type: String,
|
||||
val mid: String,
|
||||
val tid: String,
|
||||
val schemeGroup: String,
|
||||
val schemeCode: String,
|
||||
val orderId: String,
|
||||
val requestCurrency: String,
|
||||
val requestAmount: BigDecimal,
|
||||
val resultStatus: String,
|
||||
val approvalDay: String,
|
||||
val sign: String
|
||||
)
|
||||
|
||||
data class PayverseWebhookResponse(val receiveResult: String)
|
||||
|
@@ -22,6 +22,7 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.apache.commons.codec.digest.DigestUtils
|
||||
import org.json.JSONObject
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.context.ApplicationEventPublisher
|
||||
@@ -34,6 +35,7 @@ import org.springframework.transaction.annotation.Transactional
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
@@ -63,9 +65,112 @@ class ChargeService(
|
||||
@Value("\${apple.iap-verify-sandbox-url}")
|
||||
private val appleInAppVerifySandBoxUrl: String,
|
||||
@Value("\${apple.iap-verify-url}")
|
||||
private val appleInAppVerifyUrl: String
|
||||
private val appleInAppVerifyUrl: String,
|
||||
|
||||
@Value("\${payverse.mid}")
|
||||
private val payverseMid: String,
|
||||
@Value("\${payverse.client-key}")
|
||||
private val payverseClientKey: String,
|
||||
@Value("\${payverse.secret-key}")
|
||||
private val payverseSecretKey: String,
|
||||
|
||||
@Value("\${payverse.usd-mid}")
|
||||
private val payverseUsdMid: String,
|
||||
@Value("\${payverse.usd-client-key}")
|
||||
private val payverseUsdClientKey: String,
|
||||
@Value("\${payverse.usd-secret-key}")
|
||||
private val payverseUsdSecretKey: String,
|
||||
|
||||
@Value("\${payverse.host}")
|
||||
private val payverseHost: String,
|
||||
|
||||
@Value("\${server.env}")
|
||||
private val serverEnv: String
|
||||
) {
|
||||
|
||||
@Transactional
|
||||
fun payverseWebhook(request: PayverseWebhookRequest): Boolean {
|
||||
val chargeId = request.orderId.toLongOrNull() ?: return false
|
||||
val charge = chargeRepository.findByIdOrNull(chargeId) ?: return false
|
||||
|
||||
// 결제수단 확인
|
||||
if (charge.payment?.paymentGateway != PaymentGateway.PAYVERSE) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 결제 상태 분기 처리
|
||||
return when (charge.payment?.status) {
|
||||
PaymentStatus.REQUEST -> {
|
||||
// 성공 조건 검증
|
||||
val mid = if (request.requestCurrency == "KRW") {
|
||||
payverseMid
|
||||
} else {
|
||||
payverseUsdMid
|
||||
}
|
||||
val expectedSign = DigestUtils.sha512Hex(
|
||||
String.format(
|
||||
"||%s||%s||%s||%s||%s||",
|
||||
if (request.requestCurrency == "KRW") {
|
||||
payverseSecretKey
|
||||
} else {
|
||||
payverseUsdSecretKey
|
||||
},
|
||||
mid,
|
||||
request.orderId,
|
||||
request.requestAmount,
|
||||
request.approvalDay
|
||||
)
|
||||
)
|
||||
|
||||
val isAmountMatch = request.requestAmount.compareTo(
|
||||
charge.payment!!.price
|
||||
) == 0
|
||||
|
||||
val isSuccess = request.resultStatus == "SUCCESS" &&
|
||||
request.mid == mid &&
|
||||
request.orderId.toLongOrNull() == charge.id &&
|
||||
isAmountMatch &&
|
||||
request.sign == expectedSign
|
||||
|
||||
if (isSuccess) {
|
||||
// payverseVerify의 226~246 라인과 동일 처리
|
||||
charge.payment?.receiptId = request.tid
|
||||
val mappedMethod = if (request.schemeGroup == "PVKR") {
|
||||
mapPayverseSchemeToMethodByCode(request.schemeCode)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
charge.payment?.method = mappedMethod ?: request.schemeCode
|
||||
charge.payment?.status = PaymentStatus.COMPLETE
|
||||
charge.payment?.locale = request.requestCurrency
|
||||
|
||||
val member = charge.member!!
|
||||
member.charge(charge.chargeCan, charge.rewardCan, "pg")
|
||||
|
||||
applicationEventPublisher.publishEvent(
|
||||
ChargeSpringEvent(
|
||||
chargeId = charge.id!!,
|
||||
memberId = member.id!!
|
||||
)
|
||||
)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
PaymentStatus.COMPLETE -> {
|
||||
// 이미 결제가 완료된 경우 성공 처리(idempotent)
|
||||
true
|
||||
}
|
||||
|
||||
else -> {
|
||||
// 그 외 상태는 404
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun chargeByCoupon(couponNumber: String, member: Member): String {
|
||||
val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber)
|
||||
@@ -126,6 +231,177 @@ class ChargeService(
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun payverseCharge(member: Member, request: PayverseChargeRequest): PayverseChargeResponse {
|
||||
val can = canRepository.findByIdOrNull(request.canId)
|
||||
?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.")
|
||||
|
||||
val requestCurrency = can.currency
|
||||
val isKrw = requestCurrency == "KRW"
|
||||
val mid = if (isKrw) {
|
||||
payverseMid
|
||||
} else {
|
||||
payverseUsdMid
|
||||
}
|
||||
val clientKey = if (isKrw) {
|
||||
payverseClientKey
|
||||
} else {
|
||||
payverseUsdClientKey
|
||||
}
|
||||
val secretKey = if (isKrw) {
|
||||
payverseSecretKey
|
||||
} else {
|
||||
payverseUsdSecretKey
|
||||
}
|
||||
|
||||
val charge = Charge(can.can, can.rewardCan)
|
||||
charge.title = can.title
|
||||
charge.member = member
|
||||
charge.can = can
|
||||
|
||||
val payment = Payment(paymentGateway = PaymentGateway.PAYVERSE)
|
||||
payment.price = can.price
|
||||
charge.payment = payment
|
||||
|
||||
val savedCharge = chargeRepository.save(charge)
|
||||
|
||||
val chargeId = savedCharge.id!!
|
||||
val amount = BigDecimal(
|
||||
savedCharge.payment!!.price
|
||||
.setScale(4, RoundingMode.HALF_UP)
|
||||
.stripTrailingZeros()
|
||||
.toPlainString()
|
||||
)
|
||||
val reqDate = savedCharge.createdAt!!.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))
|
||||
val sign = DigestUtils.sha512Hex(
|
||||
String.format(
|
||||
"||%s||%s||%s||%s||%s||",
|
||||
secretKey,
|
||||
mid,
|
||||
chargeId,
|
||||
amount,
|
||||
reqDate
|
||||
)
|
||||
)
|
||||
val customerId = "${serverEnv}_user_${member.id!!}"
|
||||
|
||||
val payload = linkedMapOf(
|
||||
"mid" to mid,
|
||||
"clientKey" to clientKey,
|
||||
"orderId" to chargeId.toString(),
|
||||
"customerId" to customerId,
|
||||
"productName" to can.title,
|
||||
"requestCurrency" to requestCurrency,
|
||||
"requestAmount" to amount,
|
||||
"reqDate" to reqDate,
|
||||
"sign" to sign
|
||||
)
|
||||
val payloadJson = objectMapper.writeValueAsString(payload)
|
||||
|
||||
return PayverseChargeResponse(chargeId = charge.id!!, payloadJson = payloadJson)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun payverseVerify(memberId: Long, verifyRequest: PayverseVerifyRequest): ChargeCompleteResponse {
|
||||
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
|
||||
?: throw SodaException("결제정보에 오류가 있습니다.")
|
||||
val member = memberRepository.findByIdOrNull(memberId)
|
||||
?: throw SodaException("로그인 정보를 확인해주세요.")
|
||||
|
||||
val isKrw = charge.can?.currency == "KRW"
|
||||
val mid = if (isKrw) {
|
||||
payverseMid
|
||||
} else {
|
||||
payverseUsdMid
|
||||
}
|
||||
val clientKey = if (isKrw) {
|
||||
payverseClientKey
|
||||
} else {
|
||||
payverseUsdClientKey
|
||||
}
|
||||
|
||||
// 결제수단 확인
|
||||
if (charge.payment?.paymentGateway != PaymentGateway.PAYVERSE) {
|
||||
throw SodaException("결제정보에 오류가 있습니다.")
|
||||
}
|
||||
|
||||
// 결제 상태에 따른 분기 처리
|
||||
when (charge.payment?.status) {
|
||||
PaymentStatus.REQUEST -> {
|
||||
try {
|
||||
val url = "$payverseHost/payment/search/transaction/${verifyRequest.transactionId}"
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("mid", mid)
|
||||
.addHeader("clientKey", clientKey)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
if (!response.isSuccessful) {
|
||||
throw SodaException("결제정보에 오류가 있습니다.")
|
||||
}
|
||||
|
||||
val body = response.body?.string() ?: throw SodaException("결제정보에 오류가 있습니다.")
|
||||
val verifyResponse = objectMapper.readValue(body, PayverseVerifyResponse::class.java)
|
||||
|
||||
val customerId = "${serverEnv}_user_${member.id!!}"
|
||||
val isSuccess = verifyResponse.resultStatus == "SUCCESS" &&
|
||||
verifyResponse.transactionStatus == "SUCCESS" &&
|
||||
verifyResponse.orderId.toLongOrNull() == charge.id &&
|
||||
verifyResponse.customerId == customerId &&
|
||||
verifyResponse.requestAmount.compareTo(charge.can!!.price) == 0
|
||||
|
||||
if (isSuccess) {
|
||||
// verify 함수의 232~248 라인과 동일 처리
|
||||
charge.payment?.receiptId = verifyResponse.tid
|
||||
val mappedMethod = if (verifyResponse.schemeGroup == "PVKR") {
|
||||
mapPayverseSchemeToMethodByCode(verifyResponse.schemeCode)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
charge.payment?.method = mappedMethod ?: verifyResponse.schemeCode
|
||||
charge.payment?.status = PaymentStatus.COMPLETE
|
||||
// 통화코드 설정
|
||||
charge.payment?.locale = verifyResponse.requestCurrency
|
||||
|
||||
member.charge(charge.chargeCan, charge.rewardCan, "pg")
|
||||
|
||||
applicationEventPublisher.publishEvent(
|
||||
ChargeSpringEvent(
|
||||
chargeId = charge.id!!,
|
||||
memberId = member.id!!
|
||||
)
|
||||
)
|
||||
|
||||
return ChargeCompleteResponse(
|
||||
price = charge.payment!!.price,
|
||||
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
|
||||
isFirstCharged = chargeRepository.isFirstCharged(memberId)
|
||||
)
|
||||
} else {
|
||||
throw SodaException("결제정보에 오류가 있습니다.")
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
throw SodaException("결제정보에 오류가 있습니다.")
|
||||
}
|
||||
}
|
||||
|
||||
PaymentStatus.COMPLETE -> {
|
||||
// 이미 결제가 완료된 경우, 동일한 데이터로 즉시 반환
|
||||
return ChargeCompleteResponse(
|
||||
price = charge.payment!!.price,
|
||||
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
|
||||
isFirstCharged = chargeRepository.isFirstCharged(memberId)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw SodaException("결제정보에 오류가 있습니다.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun charge(member: Member, request: ChargeRequest): ChargeResponse {
|
||||
val can = canRepository.findByIdOrNull(request.canId)
|
||||
@@ -137,7 +413,7 @@ class ChargeService(
|
||||
charge.can = can
|
||||
|
||||
val payment = Payment(paymentGateway = request.paymentGateway)
|
||||
payment.price = can.price.toDouble()
|
||||
payment.price = can.price
|
||||
charge.payment = payment
|
||||
|
||||
chargeRepository.save(charge)
|
||||
@@ -176,14 +452,14 @@ class ChargeService(
|
||||
)
|
||||
|
||||
return ChargeCompleteResponse(
|
||||
price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(),
|
||||
price = charge.payment!!.price,
|
||||
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
|
||||
isFirstCharged = chargeRepository.isFirstCharged(memberId)
|
||||
)
|
||||
} else {
|
||||
throw SodaException("결제정보에 오류가 있습니다.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
throw SodaException("결제정보에 오류가 있습니다.")
|
||||
}
|
||||
} else {
|
||||
@@ -226,14 +502,14 @@ class ChargeService(
|
||||
)
|
||||
|
||||
return ChargeCompleteResponse(
|
||||
price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(),
|
||||
price = charge.payment!!.price,
|
||||
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
|
||||
isFirstCharged = chargeRepository.isFirstCharged(memberId)
|
||||
)
|
||||
} else {
|
||||
throw SodaException("결제정보에 오류가 있습니다.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
throw SodaException("결제정보에 오류가 있습니다.")
|
||||
}
|
||||
} else {
|
||||
@@ -251,7 +527,7 @@ class ChargeService(
|
||||
payment.price = if (request.price != null) {
|
||||
request.price!!
|
||||
} else {
|
||||
0.toDouble()
|
||||
0.toBigDecimal()
|
||||
}
|
||||
|
||||
payment.locale = request.locale
|
||||
@@ -286,7 +562,7 @@ class ChargeService(
|
||||
)
|
||||
|
||||
return ChargeCompleteResponse(
|
||||
price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(),
|
||||
price = charge.payment!!.price,
|
||||
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
|
||||
isFirstCharged = chargeRepository.isFirstCharged(memberId)
|
||||
)
|
||||
@@ -303,7 +579,7 @@ class ChargeService(
|
||||
member: Member,
|
||||
title: String,
|
||||
chargeCan: Int,
|
||||
price: Double,
|
||||
price: BigDecimal,
|
||||
currencyCode: String,
|
||||
productId: String,
|
||||
purchaseToken: String,
|
||||
@@ -331,8 +607,7 @@ class ChargeService(
|
||||
memberId: Long,
|
||||
chargeId: Long,
|
||||
productId: String,
|
||||
purchaseToken: String,
|
||||
paymentGateway: PaymentGateway
|
||||
purchaseToken: String
|
||||
): ChargeCompleteResponse {
|
||||
val charge = chargeRepository.findByIdOrNull(id = chargeId)
|
||||
?: throw SodaException("결제정보에 오류가 있습니다.")
|
||||
@@ -354,7 +629,7 @@ class ChargeService(
|
||||
)
|
||||
|
||||
return ChargeCompleteResponse(
|
||||
price = BigDecimal(charge.payment!!.price).setScale(2, RoundingMode.HALF_UP).toDouble(),
|
||||
price = charge.payment!!.price,
|
||||
currencyCode = charge.payment!!.locale?.takeLast(3) ?: "KRW",
|
||||
isFirstCharged = chargeRepository.isFirstCharged(memberId)
|
||||
)
|
||||
@@ -436,4 +711,13 @@ class ChargeService(
|
||||
throw SodaException("결제를 완료하지 못했습니다.")
|
||||
}
|
||||
}
|
||||
|
||||
// Payverse 결제수단 매핑: 특정 schemeCode는 "카드"로 표기, 아니면 null 반환
|
||||
private fun mapPayverseSchemeToMethodByCode(schemeCode: String?): String? {
|
||||
val cardCodes = setOf(
|
||||
"041", "044", "361", "364", "365", "366", "367", "368", "369", "370", "371", "372", "373", "374", "381",
|
||||
"218", "071", "002", "089", "045", "050", "048", "090", "092"
|
||||
)
|
||||
return if (schemeCode != null && cardCodes.contains(schemeCode)) "카드" else null
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.can.charge.temp
|
||||
|
||||
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class ChargeTempRequest(
|
||||
val can: Int,
|
||||
val price: Int,
|
||||
val price: BigDecimal,
|
||||
val paymentGateway: PaymentGateway
|
||||
)
|
||||
|
@@ -41,7 +41,7 @@ class ChargeTempService(
|
||||
charge.member = member
|
||||
|
||||
val payment = Payment(paymentGateway = request.paymentGateway)
|
||||
payment.price = request.price.toDouble()
|
||||
payment.price = request.price
|
||||
charge.payment = payment
|
||||
|
||||
chargeRepository.save(charge)
|
||||
@@ -66,7 +66,7 @@ class ChargeTempService(
|
||||
VerifyResult::class.java
|
||||
)
|
||||
|
||||
if (verifyResult.status == 1 && verifyResult.price == charge.payment!!.price.toInt()) {
|
||||
if (verifyResult.status == 1 && verifyResult.price == charge.payment!!.price) {
|
||||
charge.payment?.receiptId = verifyResult.receiptId
|
||||
charge.payment?.method = verifyResult.method
|
||||
charge.payment?.status = PaymentStatus.COMPLETE
|
||||
@@ -74,7 +74,7 @@ class ChargeTempService(
|
||||
} else {
|
||||
throw SodaException("결제정보에 오류가 있습니다.")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
throw SodaException("결제정보에 오류가 있습니다.")
|
||||
}
|
||||
} else {
|
||||
|
@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.can.payment
|
||||
|
||||
import kr.co.vividnext.sodalive.can.charge.Charge
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import java.math.BigDecimal
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.EnumType
|
||||
@@ -25,7 +26,8 @@ data class Payment(
|
||||
var receiptId: String? = null
|
||||
var method: String? = null
|
||||
|
||||
var price: Double = 0.toDouble()
|
||||
@Column(precision = 10, scale = 4, nullable = false)
|
||||
var price: BigDecimal = 0.toBigDecimal()
|
||||
var locale: String? = null
|
||||
var orderId: String? = null
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
package kr.co.vividnext.sodalive.can.payment
|
||||
|
||||
enum class PaymentGateway {
|
||||
PG, GOOGLE_IAP, APPLE_IAP, POINT_CLICK_AD
|
||||
PG, PAYVERSE, GOOGLE_IAP, APPLE_IAP, POINT_CLICK_AD
|
||||
}
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.chat.original.controller
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.dto.Character
|
||||
import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkCharactersPageResponse
|
||||
import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkDetailResponse
|
||||
import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkListItemResponse
|
||||
import kr.co.vividnext.sodalive.chat.original.dto.OriginalWorkListResponse
|
||||
@@ -79,37 +78,4 @@ class OriginalWorkController(
|
||||
val response = OriginalWorkDetailResponse.from(ow, imageHost, characters)
|
||||
ApiResponse.ok(response)
|
||||
}
|
||||
|
||||
/**
|
||||
* 지정 원작에 속한 활성 캐릭터 목록 조회 (페이징)
|
||||
* - 로그인 및 본인인증 필수
|
||||
* - 기본 페이지 사이즈 20
|
||||
*/
|
||||
@GetMapping("/{id}/characters")
|
||||
fun listCharacters(
|
||||
@PathVariable id: Long,
|
||||
@RequestParam(defaultValue = "0") page: Int,
|
||||
@RequestParam(defaultValue = "20") size: Int,
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||
) = run {
|
||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||
|
||||
val pageRes = queryService.getActiveCharactersPage(id, page, size)
|
||||
val content = pageRes.content.map {
|
||||
val path = it.imagePath ?: "profile/default-profile.png"
|
||||
Character(
|
||||
characterId = it.id!!,
|
||||
name = it.name,
|
||||
description = it.description,
|
||||
imageUrl = "$imageHost/$path"
|
||||
)
|
||||
}
|
||||
ApiResponse.ok(
|
||||
OriginalWorkCharactersPageResponse(
|
||||
totalCount = pageRes.totalElements,
|
||||
content = content
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,8 @@
|
||||
package kr.co.vividnext.sodalive.common
|
||||
|
||||
const val WAF_GEO_HEADER = "x-amzn-waf-geo-country"
|
||||
|
||||
enum class GeoCountry { KR, OTHER }
|
||||
|
||||
fun parseGeo(headerValue: String?): GeoCountry =
|
||||
if (headerValue?.trim()?.uppercase() == "KR") GeoCountry.KR else GeoCountry.OTHER
|
@@ -0,0 +1,20 @@
|
||||
package kr.co.vividnext.sodalive.common
|
||||
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.filter.OncePerRequestFilter
|
||||
import javax.servlet.FilterChain
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@Component
|
||||
class GeoCountryFilter : OncePerRequestFilter() {
|
||||
override fun doFilterInternal(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
filterChain: FilterChain
|
||||
) {
|
||||
val country = parseGeo(request.getHeader(WAF_GEO_HEADER))
|
||||
request.setAttribute("geoCountry", country)
|
||||
filterChain.doFilter(request, response)
|
||||
}
|
||||
}
|
@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
|
||||
@RestControllerAdvice
|
||||
class SodaExceptionHandler {
|
||||
@@ -63,6 +64,7 @@ class SodaExceptionHandler {
|
||||
|
||||
@ExceptionHandler(Exception::class)
|
||||
fun handleException(e: Exception) = run {
|
||||
if (e is ResponseStatusException) throw e
|
||||
logger.error("API error", e)
|
||||
ApiResponse.error("알 수 없는 오류가 발생했습니다. 다시 시도해 주세요.")
|
||||
}
|
||||
|
@@ -96,6 +96,7 @@ class SecurityConfig(
|
||||
.antMatchers(HttpMethod.GET, "/api/chat/character/main").permitAll()
|
||||
.antMatchers(HttpMethod.GET, "/api/chat/room/list").permitAll()
|
||||
.antMatchers(HttpMethod.GET, "/api/chat/original/list").permitAll()
|
||||
.antMatchers(HttpMethod.POST, "/charge/payverse/webhook").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.build()
|
||||
|
@@ -53,7 +53,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
|
||||
.innerJoin(useCan.room, liveRoom)
|
||||
.innerJoin(liveRoom.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
useCan.isRefund.isFalse
|
||||
.and(useCan.createdAt.goe(startDate))
|
||||
@@ -119,7 +122,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
|
||||
.innerJoin(order.audioContent, audioContent)
|
||||
.innerJoin(audioContent.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
order.createdAt.goe(startDate)
|
||||
.and(order.createdAt.loe(endDate))
|
||||
@@ -196,7 +202,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
|
||||
.innerJoin(order.audioContent, audioContent)
|
||||
.innerJoin(audioContent.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
audioContent.member.id.eq(memberId)
|
||||
.and(order.isActive.isTrue)
|
||||
@@ -318,7 +327,10 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac
|
||||
.innerJoin(useCan.communityPost, creatorCommunity)
|
||||
.innerJoin(creatorCommunity.member, member)
|
||||
.leftJoin(creatorSettlementRatio)
|
||||
.on(member.id.eq(creatorSettlementRatio.member.id))
|
||||
.on(
|
||||
member.id.eq(creatorSettlementRatio.member.id)
|
||||
.and(creatorSettlementRatio.deletedAt.isNull)
|
||||
)
|
||||
.where(
|
||||
useCan.isRefund.isFalse
|
||||
.and(useCan.canUsage.eq(CanUsage.PAID_COMMUNITY_POST))
|
||||
|
@@ -1,6 +1,8 @@
|
||||
package kr.co.vividnext.sodalive.marketing
|
||||
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.EnumType
|
||||
import javax.persistence.Enumerated
|
||||
@@ -19,7 +21,8 @@ data class AdTrackingHistory(
|
||||
val pidName: String,
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
val type: AdTrackingHistoryType,
|
||||
val price: Double = 0.toDouble(),
|
||||
@Column(precision = 10, scale = 4, nullable = false)
|
||||
val price: BigDecimal = 0.toBigDecimal(),
|
||||
val locale: String? = null,
|
||||
val memberId: Long,
|
||||
val createdAt: LocalDateTime = LocalDateTime.now(),
|
||||
|
@@ -4,6 +4,7 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.springframework.stereotype.Service
|
||||
import java.math.BigDecimal
|
||||
|
||||
@Service
|
||||
class AdTrackingService(
|
||||
@@ -17,7 +18,7 @@ class AdTrackingService(
|
||||
pid: String,
|
||||
type: AdTrackingHistoryType,
|
||||
memberId: Long,
|
||||
price: Double? = null,
|
||||
price: BigDecimal? = null,
|
||||
locale: String? = null
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
@@ -30,7 +31,7 @@ class AdTrackingService(
|
||||
pid = pid,
|
||||
pidName = mediaPartner.pidName,
|
||||
type = type,
|
||||
price = price ?: 0.toDouble(),
|
||||
price = price ?: 0.toBigDecimal(),
|
||||
locale = locale,
|
||||
memberId = memberId
|
||||
)
|
||||
|
@@ -1,6 +1,7 @@
|
||||
server:
|
||||
shutdown: graceful
|
||||
env: ${SERVER_ENV}
|
||||
forward-headers-strategy: framework
|
||||
|
||||
logging:
|
||||
level:
|
||||
@@ -13,6 +14,16 @@ weraser:
|
||||
apiUrl: ${WERASER_API_URL}
|
||||
apiKey: ${WERASER_API_KEY}
|
||||
|
||||
payverse:
|
||||
host: ${PAYVERSE_HOST}
|
||||
inboundIp: ${PAYVERSE_INBOUND_IP}
|
||||
mid: ${PAYVERSE_MID}
|
||||
clientKey: ${PAYVERSE_CLIENT_KEY}
|
||||
secretKey: ${PAYVERSE_SECRET_KEY}
|
||||
usdMid: ${PAYVERSE_USD_MID}
|
||||
usdClientKey: ${PAYVERSE_USD_CLIENT_KEY}
|
||||
usdSecretKey: ${PAYVERSE_USD_SECRET_KEY}
|
||||
|
||||
bootpay:
|
||||
applicationId: ${BOOTPAY_APPLICATION_ID}
|
||||
privateKey: ${BOOTPAY_PRIVATE_KEY}
|
||||
|
Reference in New Issue
Block a user