코인 충전, 코인 내역 API 추가
This commit is contained in:
parent
c06de5f9f6
commit
7c8084bdd4
|
@ -51,6 +51,9 @@ dependencies {
|
||||||
// bootpay
|
// bootpay
|
||||||
implementation("io.github.bootpay:backend:2.2.1")
|
implementation("io.github.bootpay:backend:2.2.1")
|
||||||
|
|
||||||
|
implementation("com.squareup.okhttp3:okhttp:4.9.3")
|
||||||
|
implementation("org.json:json:20230227")
|
||||||
|
|
||||||
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
developmentOnly("org.springframework.boot:spring-boot-devtools")
|
||||||
runtimeOnly("com.h2database:h2")
|
runtimeOnly("com.h2database:h2")
|
||||||
runtimeOnly("com.mysql:mysql-connector-j")
|
runtimeOnly("com.mysql:mysql-connector-j")
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package kr.co.vividnext.sodalive.can
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.EnumType
|
||||||
|
import javax.persistence.Enumerated
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class Can(
|
||||||
|
var title: String,
|
||||||
|
var can: Int,
|
||||||
|
var rewardCan: Int,
|
||||||
|
var price: Int,
|
||||||
|
@Enumerated(value = EnumType.STRING)
|
||||||
|
var status: CanStatus
|
||||||
|
) : BaseEntity()
|
||||||
|
|
||||||
|
enum class CanStatus {
|
||||||
|
SALE, END_OF_SALE
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
package kr.co.vividnext.sodalive.can
|
||||||
|
|
||||||
|
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.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
|
||||||
|
@RequestMapping("/can")
|
||||||
|
class CanController(private val service: CanService) {
|
||||||
|
@GetMapping
|
||||||
|
fun getCans(): ApiResponse<List<CanResponse>> {
|
||||||
|
return ApiResponse.ok(service.getCans())
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/status")
|
||||||
|
fun getCanStatus(
|
||||||
|
@RequestParam container: String,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) {
|
||||||
|
throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiResponse.ok(service.getCanStatus(member, container))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/status/use")
|
||||||
|
fun getCanUseStatus(
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||||
|
@RequestParam("timezone") timezone: String,
|
||||||
|
@RequestParam("container") container: String,
|
||||||
|
pageable: Pageable
|
||||||
|
) = run {
|
||||||
|
if (member == null) {
|
||||||
|
throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiResponse.ok(service.getCanUseStatus(member, pageable, timezone, container))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/status/charge")
|
||||||
|
fun getCanChargeStatus(
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||||
|
@RequestParam("timezone") timezone: String,
|
||||||
|
@RequestParam("container") container: String,
|
||||||
|
pageable: Pageable
|
||||||
|
) = run {
|
||||||
|
if (member == null) {
|
||||||
|
throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiResponse.ok(service.getCanChargeStatus(member, pageable, timezone, container))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package kr.co.vividnext.sodalive.can
|
||||||
|
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
|
import kr.co.vividnext.sodalive.can.QCan.can1
|
||||||
|
import kr.co.vividnext.sodalive.can.charge.Charge
|
||||||
|
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
|
||||||
|
import kr.co.vividnext.sodalive.can.charge.QCharge.charge
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.QPayment.payment
|
||||||
|
import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
|
||||||
|
import kr.co.vividnext.sodalive.can.use.UseCan
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.QMember
|
||||||
|
import org.springframework.data.domain.Pageable
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface CanRepository : JpaRepository<Can, Long>, CanQueryRepository
|
||||||
|
|
||||||
|
interface CanQueryRepository {
|
||||||
|
fun findAllByStatus(status: CanStatus): List<CanResponse>
|
||||||
|
fun getCanUseStatus(member: Member, pageable: Pageable): List<UseCan>
|
||||||
|
fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List<Charge>
|
||||||
|
}
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class CanQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanQueryRepository {
|
||||||
|
override fun findAllByStatus(status: CanStatus): List<CanResponse> {
|
||||||
|
return queryFactory
|
||||||
|
.select(
|
||||||
|
QCanResponse(
|
||||||
|
can1.id,
|
||||||
|
can1.title,
|
||||||
|
can1.can,
|
||||||
|
can1.rewardCan,
|
||||||
|
can1.price
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(can1)
|
||||||
|
.where(can1.status.eq(status))
|
||||||
|
.orderBy(can1.can.asc())
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCanUseStatus(member: Member, pageable: Pageable): List<UseCan> {
|
||||||
|
return queryFactory
|
||||||
|
.selectFrom(useCan)
|
||||||
|
.where(useCan.member.id.eq(member.id))
|
||||||
|
.offset(pageable.offset)
|
||||||
|
.limit(pageable.pageSize.toLong())
|
||||||
|
.orderBy(useCan.id.desc())
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCanChargeStatus(member: Member, pageable: Pageable, container: String): List<Charge> {
|
||||||
|
val qMember = QMember.member
|
||||||
|
val chargeStatusCondition = when (container) {
|
||||||
|
"aos" -> {
|
||||||
|
charge.payment.paymentGateway.eq(PaymentGateway.PG)
|
||||||
|
.or(charge.payment.paymentGateway.eq(PaymentGateway.GOOGLE_IAP))
|
||||||
|
}
|
||||||
|
|
||||||
|
"ios" -> {
|
||||||
|
charge.payment.paymentGateway.eq(PaymentGateway.PG)
|
||||||
|
.or(charge.payment.paymentGateway.eq(PaymentGateway.APPLE_IAP))
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> charge.payment.paymentGateway.eq(PaymentGateway.PG)
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryFactory
|
||||||
|
.selectFrom(charge)
|
||||||
|
.innerJoin(charge.member, qMember)
|
||||||
|
.innerJoin(charge.useCan, useCan)
|
||||||
|
.leftJoin(charge.payment, payment)
|
||||||
|
.where(
|
||||||
|
qMember.id.eq(member.id)
|
||||||
|
.and(
|
||||||
|
payment.status.eq(PaymentStatus.COMPLETE)
|
||||||
|
.or(
|
||||||
|
charge.status.eq(ChargeStatus.REFUND_CHARGE)
|
||||||
|
.and(useCan.isNotNull)
|
||||||
|
)
|
||||||
|
.or(charge.status.eq(ChargeStatus.EVENT))
|
||||||
|
.or(charge.status.eq(ChargeStatus.ADMIN))
|
||||||
|
)
|
||||||
|
.and(chargeStatusCondition)
|
||||||
|
)
|
||||||
|
.offset(pageable.offset)
|
||||||
|
.limit(pageable.pageSize.toLong())
|
||||||
|
.orderBy(charge.id.desc())
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package kr.co.vividnext.sodalive.can
|
||||||
|
|
||||||
|
import com.querydsl.core.annotations.QueryProjection
|
||||||
|
|
||||||
|
data class CanResponse @QueryProjection constructor(
|
||||||
|
val id: Long,
|
||||||
|
val title: String,
|
||||||
|
val can: Int,
|
||||||
|
val rewardCan: Int,
|
||||||
|
val price: Int
|
||||||
|
)
|
|
@ -0,0 +1,118 @@
|
||||||
|
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.member.Member
|
||||||
|
import org.springframework.data.domain.Pageable
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class CanService(private val repository: CanRepository) {
|
||||||
|
fun getCans(): List<CanResponse> {
|
||||||
|
return repository.findAllByStatus(status = CanStatus.SALE)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCanStatus(member: Member, container: String): GetCanStatusResponse {
|
||||||
|
return GetCanStatusResponse(
|
||||||
|
chargeCan = member.getChargeCan(container),
|
||||||
|
rewardCan = member.getRewardCan(container)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCanUseStatus(
|
||||||
|
member: Member,
|
||||||
|
pageable: Pageable,
|
||||||
|
timezone: String,
|
||||||
|
container: String
|
||||||
|
): List<GetCanUseStatusResponseItem> {
|
||||||
|
return repository.getCanUseStatus(member, pageable)
|
||||||
|
.filter { (it.can + it.rewardCan) > 0 }
|
||||||
|
.filter {
|
||||||
|
when (container) {
|
||||||
|
"aos" -> {
|
||||||
|
it.useCanCalculates.any { useCanCalculate ->
|
||||||
|
useCanCalculate.paymentGateway == PaymentGateway.PG ||
|
||||||
|
useCanCalculate.paymentGateway == PaymentGateway.GOOGLE_IAP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"ios" -> {
|
||||||
|
it.useCanCalculates.any { useCanCalculate ->
|
||||||
|
useCanCalculate.paymentGateway == PaymentGateway.PG ||
|
||||||
|
useCanCalculate.paymentGateway == PaymentGateway.APPLE_IAP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> it.useCanCalculates.any { useCanCalculate ->
|
||||||
|
useCanCalculate.paymentGateway == PaymentGateway.PG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
val title: String = when (it.canUsage) {
|
||||||
|
CanUsage.DONATION -> {
|
||||||
|
"[후원] ${it.room!!.member!!.nickname}"
|
||||||
|
}
|
||||||
|
|
||||||
|
CanUsage.LIVE -> {
|
||||||
|
"[라이브] ${it.room!!.title}"
|
||||||
|
}
|
||||||
|
|
||||||
|
CanUsage.CHANGE_NICKNAME -> "닉네임 변경"
|
||||||
|
CanUsage.ORDER_CONTENT -> "콘텐츠 구매"
|
||||||
|
}
|
||||||
|
|
||||||
|
val createdAt = it.createdAt!!
|
||||||
|
.atZone(ZoneId.of("UTC"))
|
||||||
|
.withZoneSameInstant(ZoneId.of(timezone))
|
||||||
|
|
||||||
|
GetCanUseStatusResponseItem(
|
||||||
|
title = title,
|
||||||
|
date = createdAt.format(
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd | HH:mm:ss")
|
||||||
|
),
|
||||||
|
can = it.can + it.rewardCan
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCanChargeStatus(
|
||||||
|
member: Member,
|
||||||
|
pageable: Pageable,
|
||||||
|
timezone: String,
|
||||||
|
container: String
|
||||||
|
): List<GetCanChargeStatusResponseItem> {
|
||||||
|
return repository.getCanChargeStatus(member, pageable, container)
|
||||||
|
.map {
|
||||||
|
val canTitle = it.title ?: ""
|
||||||
|
val chargeMethod = when (it.status) {
|
||||||
|
ChargeStatus.CHARGE, ChargeStatus.EVENT -> {
|
||||||
|
it.payment!!.method ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
ChargeStatus.REFUND_CHARGE -> {
|
||||||
|
"환불"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
"환불"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val createdAt = it.createdAt!!
|
||||||
|
.atZone(ZoneId.of("UTC"))
|
||||||
|
.withZoneSameInstant(ZoneId.of(timezone))
|
||||||
|
|
||||||
|
GetCanChargeStatusResponseItem(
|
||||||
|
canTitle = canTitle,
|
||||||
|
date = createdAt.format(
|
||||||
|
DateTimeFormatter.ofPattern("yyyy-MM-dd | HH:mm:ss")
|
||||||
|
),
|
||||||
|
chargeMethod = chargeMethod
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package kr.co.vividnext.sodalive.can
|
||||||
|
|
||||||
|
data class GetCanChargeStatusResponseItem(
|
||||||
|
val canTitle: String,
|
||||||
|
val date: String,
|
||||||
|
val chargeMethod: String
|
||||||
|
)
|
|
@ -0,0 +1,6 @@
|
||||||
|
package kr.co.vividnext.sodalive.can
|
||||||
|
|
||||||
|
data class GetCanStatusResponse(
|
||||||
|
val chargeCan: Int,
|
||||||
|
val rewardCan: Int
|
||||||
|
)
|
|
@ -0,0 +1,7 @@
|
||||||
|
package kr.co.vividnext.sodalive.can
|
||||||
|
|
||||||
|
data class GetCanUseStatusResponseItem(
|
||||||
|
val title: String,
|
||||||
|
val date: String,
|
||||||
|
val can: Int
|
||||||
|
)
|
|
@ -0,0 +1,45 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.charge
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.can.Can
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.Payment
|
||||||
|
import kr.co.vividnext.sodalive.can.use.UseCan
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import javax.persistence.CascadeType
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.EnumType
|
||||||
|
import javax.persistence.Enumerated
|
||||||
|
import javax.persistence.FetchType
|
||||||
|
import javax.persistence.JoinColumn
|
||||||
|
import javax.persistence.ManyToOne
|
||||||
|
import javax.persistence.OneToOne
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class Charge(
|
||||||
|
var chargeCan: Int,
|
||||||
|
var rewardCan: Int,
|
||||||
|
@Enumerated(value = EnumType.STRING)
|
||||||
|
var status: ChargeStatus = ChargeStatus.CHARGE
|
||||||
|
) : BaseEntity() {
|
||||||
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "can_id", nullable = true)
|
||||||
|
var can: Can? = null
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "member_id", nullable = false)
|
||||||
|
var member: Member? = null
|
||||||
|
|
||||||
|
@OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
|
||||||
|
@JoinColumn(name = "payment_id", nullable = true)
|
||||||
|
var payment: Payment? = null
|
||||||
|
set(value) {
|
||||||
|
value?.charge = this
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
@OneToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
|
||||||
|
@JoinColumn(name = "use_can_id", nullable = true)
|
||||||
|
var useCan: UseCan? = null
|
||||||
|
|
||||||
|
var title: String? = null
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.charge
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
|
import org.springframework.security.core.userdetails.User
|
||||||
|
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
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/charge")
|
||||||
|
class ChargeController(private val service: ChargeService) {
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
fun charge(
|
||||||
|
@RequestBody chargeRequest: ChargeRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) {
|
||||||
|
throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiResponse.ok(service.charge(member, chargeRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/verify")
|
||||||
|
fun verify(
|
||||||
|
@RequestBody verifyRequest: VerifyRequest,
|
||||||
|
@AuthenticationPrincipal user: User
|
||||||
|
) = ApiResponse.ok(service.verify(user, verifyRequest))
|
||||||
|
|
||||||
|
@PostMapping("/apple")
|
||||||
|
fun appleCharge(
|
||||||
|
@RequestBody chargeRequest: AppleChargeRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) {
|
||||||
|
throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiResponse.ok(service.appleCharge(member, chargeRequest))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/apple/verify")
|
||||||
|
fun appleVerify(
|
||||||
|
@RequestBody verifyRequest: AppleVerifyRequest,
|
||||||
|
@AuthenticationPrincipal user: User
|
||||||
|
) = ApiResponse.ok(service.appleVerify(user, verifyRequest))
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.charge
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||||
|
|
||||||
|
data class ChargeRequest(val canId: Long, val paymentGateway: PaymentGateway)
|
||||||
|
|
||||||
|
data class ChargeResponse(val chargeId: Long)
|
||||||
|
|
||||||
|
data class VerifyRequest(
|
||||||
|
@JsonProperty("receipt_id")
|
||||||
|
val receiptId: String,
|
||||||
|
@JsonProperty("order_id")
|
||||||
|
val orderId: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class VerifyResult(
|
||||||
|
@JsonProperty("receipt_id")
|
||||||
|
val receiptId: String,
|
||||||
|
val method: String,
|
||||||
|
val status: Int,
|
||||||
|
val price: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AppleChargeRequest(
|
||||||
|
val title: String,
|
||||||
|
val chargeCan: Int,
|
||||||
|
val paymentGateway: PaymentGateway,
|
||||||
|
var price: Double? = null,
|
||||||
|
var locale: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AppleVerifyRequest(val receiptString: String, val chargeId: Long)
|
||||||
|
|
||||||
|
data class AppleVerifyResponse(val status: Int)
|
|
@ -0,0 +1,7 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.charge
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface ChargeRepository : JpaRepository<Charge, Long>
|
|
@ -0,0 +1,203 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.charge
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import kr.co.bootpay.Bootpay
|
||||||
|
import kr.co.vividnext.sodalive.can.CanRepository
|
||||||
|
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.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import org.json.JSONObject
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.security.core.userdetails.User
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
class ChargeService(
|
||||||
|
private val chargeRepository: ChargeRepository,
|
||||||
|
private val canRepository: CanRepository,
|
||||||
|
private val memberRepository: MemberRepository,
|
||||||
|
private val objectMapper: ObjectMapper,
|
||||||
|
private val okHttpClient: OkHttpClient,
|
||||||
|
@Value("\${bootpay.application-id}")
|
||||||
|
private val bootpayApplicationId: String,
|
||||||
|
@Value("\${bootpay.private-key}")
|
||||||
|
private val bootpayPrivateKey: String,
|
||||||
|
@Value("\${apple.iap-verify-sandbox-url}")
|
||||||
|
private val appleInAppVerifySandBoxUrl: String,
|
||||||
|
@Value("\${apple.iap-verify-url}")
|
||||||
|
private val appleInAppVerifyUrl: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun charge(member: Member, request: ChargeRequest): ChargeResponse {
|
||||||
|
val can = canRepository.findByIdOrNull(request.canId)
|
||||||
|
?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.")
|
||||||
|
|
||||||
|
val charge = Charge(can.can, can.rewardCan)
|
||||||
|
charge.title = can.title
|
||||||
|
charge.member = member
|
||||||
|
charge.can = can
|
||||||
|
|
||||||
|
val payment = Payment(paymentGateway = request.paymentGateway)
|
||||||
|
payment.price = can.price.toDouble()
|
||||||
|
charge.payment = payment
|
||||||
|
|
||||||
|
chargeRepository.save(charge)
|
||||||
|
|
||||||
|
return ChargeResponse(chargeId = charge.id!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun verify(user: User, verifyRequest: VerifyRequest) {
|
||||||
|
val charge = chargeRepository.findByIdOrNull(verifyRequest.orderId.toLong())
|
||||||
|
?: throw SodaException("결제정보에 오류가 있습니다.")
|
||||||
|
val member = memberRepository.findByEmail(user.username)
|
||||||
|
?: throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
if (charge.payment!!.paymentGateway == PaymentGateway.PG) {
|
||||||
|
val bootpay = Bootpay(bootpayApplicationId, bootpayPrivateKey)
|
||||||
|
|
||||||
|
try {
|
||||||
|
bootpay.accessToken
|
||||||
|
val verifyResult = objectMapper.convertValue(
|
||||||
|
bootpay.getReceipt(verifyRequest.receiptId),
|
||||||
|
VerifyResult::class.java
|
||||||
|
)
|
||||||
|
|
||||||
|
if (verifyResult.status == 1 && verifyResult.price == charge.can?.price) {
|
||||||
|
charge.payment?.receiptId = verifyResult.receiptId
|
||||||
|
charge.payment?.method = verifyResult.method
|
||||||
|
charge.payment?.status = PaymentStatus.COMPLETE
|
||||||
|
member.charge(charge.chargeCan, charge.rewardCan, "pg")
|
||||||
|
} else {
|
||||||
|
throw SodaException("결제정보에 오류가 있습니다.")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw SodaException("결제정보에 오류가 있습니다.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw SodaException("결제정보에 오류가 있습니다.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun appleCharge(member: Member, request: AppleChargeRequest): ChargeResponse {
|
||||||
|
val charge = Charge(request.chargeCan, 0)
|
||||||
|
charge.title = request.title
|
||||||
|
charge.member = member
|
||||||
|
|
||||||
|
val payment = Payment(paymentGateway = request.paymentGateway)
|
||||||
|
payment.price = if (request.price != null) {
|
||||||
|
request.price!!
|
||||||
|
} else {
|
||||||
|
0.toDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
payment.locale = request.locale
|
||||||
|
|
||||||
|
charge.payment = payment
|
||||||
|
|
||||||
|
chargeRepository.save(charge)
|
||||||
|
|
||||||
|
return ChargeResponse(chargeId = charge.id!!)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun appleVerify(user: User, verifyRequest: AppleVerifyRequest) {
|
||||||
|
val charge = chargeRepository.findByIdOrNull(verifyRequest.chargeId)
|
||||||
|
?: throw SodaException("결제정보에 오류가 있습니다.")
|
||||||
|
val member = memberRepository.findByEmail(user.username)
|
||||||
|
?: throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
if (charge.payment!!.paymentGateway == PaymentGateway.APPLE_IAP) {
|
||||||
|
// 검증로직
|
||||||
|
if (requestRealServerVerify(verifyRequest)) {
|
||||||
|
charge.payment?.receiptId = verifyRequest.receiptString
|
||||||
|
charge.payment?.method = "애플(인 앱 결제)"
|
||||||
|
charge.payment?.status = PaymentStatus.COMPLETE
|
||||||
|
member.charge(charge.chargeCan, charge.rewardCan, "ios")
|
||||||
|
} else {
|
||||||
|
throw SodaException("결제정보에 오류가 있습니다.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw SodaException("결제정보에 오류가 있습니다.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestRealServerVerify(verifyRequest: AppleVerifyRequest): Boolean {
|
||||||
|
val body = JSONObject()
|
||||||
|
body.put("receipt-data", verifyRequest.receiptString)
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(appleInAppVerifyUrl)
|
||||||
|
.addHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
||||||
|
.post(body.toString().toRequestBody("application/json".toMediaTypeOrNull()))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = okHttpClient.newCall(request).execute()
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val responseString = response.body?.string()
|
||||||
|
if (responseString != null) {
|
||||||
|
val verifyResult = objectMapper.readValue(responseString, AppleVerifyResponse::class.java)
|
||||||
|
return when (verifyResult.status) {
|
||||||
|
0 -> {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
21007 -> {
|
||||||
|
requestSandboxServerVerify(verifyRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw SodaException("결제정보에 오류가 있습니다.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw SodaException("결제를 완료하지 못했습니다.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw SodaException("결제를 완료하지 못했습니다.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requestSandboxServerVerify(verifyRequest: AppleVerifyRequest): Boolean {
|
||||||
|
val body = JSONObject()
|
||||||
|
body.put("receipt-data", verifyRequest.receiptString)
|
||||||
|
val request = Request.Builder()
|
||||||
|
.url(appleInAppVerifySandBoxUrl)
|
||||||
|
.addHeader(HttpHeaders.CONTENT_TYPE, "application/json")
|
||||||
|
.post(body.toString().toRequestBody("application/json".toMediaTypeOrNull()))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response = okHttpClient.newCall(request).execute()
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val responseString = response.body?.string()
|
||||||
|
if (responseString != null) {
|
||||||
|
val verifyResult = objectMapper.readValue(responseString, AppleVerifyResponse::class.java)
|
||||||
|
return when (verifyResult.status) {
|
||||||
|
0 -> {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
throw SodaException("결제정보에 오류가 있습니다.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw SodaException("결제를 완료하지 못했습니다.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw SodaException("결제를 완료하지 못했습니다.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.charge
|
||||||
|
|
||||||
|
enum class ChargeStatus {
|
||||||
|
CHARGE, REFUND_CHARGE, EVENT, CANCEL,
|
||||||
|
|
||||||
|
// 관리자 지급
|
||||||
|
ADMIN
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.payment
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.can.charge.Charge
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.Column
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.EnumType
|
||||||
|
import javax.persistence.Enumerated
|
||||||
|
import javax.persistence.FetchType
|
||||||
|
import javax.persistence.OneToOne
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class Payment(
|
||||||
|
@Enumerated(value = EnumType.STRING)
|
||||||
|
var status: PaymentStatus = PaymentStatus.REQUEST,
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Enumerated(value = EnumType.STRING)
|
||||||
|
val paymentGateway: PaymentGateway
|
||||||
|
) : BaseEntity() {
|
||||||
|
@OneToOne(mappedBy = "payment", fetch = FetchType.LAZY)
|
||||||
|
var charge: Charge? = null
|
||||||
|
|
||||||
|
@Column(columnDefinition = "TEXT", nullable = true)
|
||||||
|
var receiptId: String? = null
|
||||||
|
var method: String? = null
|
||||||
|
|
||||||
|
var price: Double = 0.toDouble()
|
||||||
|
var locale: String? = null
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PaymentStatus {
|
||||||
|
// 결제요청
|
||||||
|
REQUEST,
|
||||||
|
|
||||||
|
// 결제완료
|
||||||
|
COMPLETE,
|
||||||
|
|
||||||
|
// 환불
|
||||||
|
RETURN
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.payment
|
||||||
|
|
||||||
|
enum class PaymentGateway {
|
||||||
|
PG, GOOGLE_IAP, APPLE_IAP
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.use
|
||||||
|
|
||||||
|
enum class CanUsage {
|
||||||
|
LIVE,
|
||||||
|
DONATION,
|
||||||
|
CHANGE_NICKNAME,
|
||||||
|
ORDER_CONTENT
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.use
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import kr.co.vividnext.sodalive.live.room.LiveRoom
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import javax.persistence.CascadeType
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.EnumType
|
||||||
|
import javax.persistence.Enumerated
|
||||||
|
import javax.persistence.FetchType
|
||||||
|
import javax.persistence.JoinColumn
|
||||||
|
import javax.persistence.ManyToOne
|
||||||
|
import javax.persistence.OneToMany
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class UseCan(
|
||||||
|
@Enumerated(value = EnumType.STRING)
|
||||||
|
val canUsage: CanUsage,
|
||||||
|
|
||||||
|
val can: Int,
|
||||||
|
|
||||||
|
val rewardCan: Int,
|
||||||
|
|
||||||
|
var isRefund: Boolean = false
|
||||||
|
) : BaseEntity() {
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "member_id", nullable = false)
|
||||||
|
var member: Member? = null
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "room_id", nullable = true)
|
||||||
|
var room: LiveRoom? = null
|
||||||
|
set(value) {
|
||||||
|
value?.useCan?.add(this)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "useCan", cascade = [CascadeType.ALL])
|
||||||
|
val useCanCalculates: MutableList<UseCanCalculate> = mutableListOf()
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.use
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.CascadeType
|
||||||
|
import javax.persistence.Column
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.EnumType
|
||||||
|
import javax.persistence.Enumerated
|
||||||
|
import javax.persistence.FetchType
|
||||||
|
import javax.persistence.JoinColumn
|
||||||
|
import javax.persistence.ManyToOne
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class UseCanCalculate(
|
||||||
|
val can: Int,
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Enumerated(value = EnumType.STRING)
|
||||||
|
val paymentGateway: PaymentGateway,
|
||||||
|
|
||||||
|
@Column(nullable = false)
|
||||||
|
@Enumerated(value = EnumType.STRING)
|
||||||
|
var status: UseCanCalculateStatus
|
||||||
|
) : BaseEntity() {
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, cascade = [CascadeType.ALL])
|
||||||
|
@JoinColumn(name = "use_can_id", nullable = false)
|
||||||
|
var useCan: UseCan? = null
|
||||||
|
set(value) {
|
||||||
|
value?.useCanCalculates?.add(this)
|
||||||
|
field = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class UseCanCalculateStatus {
|
||||||
|
RECEIVED, CALCULATE_COMPLETE, REFUND
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package kr.co.vividnext.sodalive.configs
|
||||||
|
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class OkHttpConfig {
|
||||||
|
@Bean("okHttpClient")
|
||||||
|
fun okHttpClient(): OkHttpClient {
|
||||||
|
return OkHttpClient()
|
||||||
|
.newBuilder().apply {
|
||||||
|
// 서버 연결을 최대 60초 수행
|
||||||
|
connectTimeout(60, TimeUnit.SECONDS)
|
||||||
|
// 서버 요청을 최대 60초 수행
|
||||||
|
writeTimeout(60, TimeUnit.SECONDS)
|
||||||
|
// 서버 응답을 최대 60초 기다림
|
||||||
|
readTimeout(60, TimeUnit.SECONDS)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package kr.co.vividnext.sodalive.live.room
|
package kr.co.vividnext.sodalive.live.room
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.can.use.UseCan
|
||||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
@ -9,6 +10,7 @@ import javax.persistence.EnumType
|
||||||
import javax.persistence.Enumerated
|
import javax.persistence.Enumerated
|
||||||
import javax.persistence.FetchType
|
import javax.persistence.FetchType
|
||||||
import javax.persistence.JoinColumn
|
import javax.persistence.JoinColumn
|
||||||
|
import javax.persistence.OneToMany
|
||||||
import javax.persistence.OneToOne
|
import javax.persistence.OneToOne
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
|
@ -31,6 +33,9 @@ data class LiveRoom(
|
||||||
@JoinColumn(name = "member_id", nullable = false)
|
@JoinColumn(name = "member_id", nullable = false)
|
||||||
var member: Member? = null
|
var member: Member? = null
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "room", fetch = FetchType.LAZY)
|
||||||
|
var useCan: MutableList<UseCan> = mutableListOf()
|
||||||
|
|
||||||
var channelName: String? = null
|
var channelName: String? = null
|
||||||
var isActive: Boolean = true
|
var isActive: Boolean = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,8 +56,8 @@ data class Member(
|
||||||
private var pgRewardCan: Int = 0
|
private var pgRewardCan: Int = 0
|
||||||
private var googleChargeCan: Int = 0
|
private var googleChargeCan: Int = 0
|
||||||
private var googleRewardCan: Int = 0
|
private var googleRewardCan: Int = 0
|
||||||
private var appleChargeCan: Int = 0
|
var appleChargeCan: Int = 0
|
||||||
private var appleRewardCan: Int = 0
|
var appleRewardCan: Int = 0
|
||||||
|
|
||||||
fun getChargeCan(container: String): Int {
|
fun getChargeCan(container: String): Int {
|
||||||
return when (container) {
|
return when (container) {
|
||||||
|
|
|
@ -12,6 +12,10 @@ bootpay:
|
||||||
applicationId: ${BOOTPAY_APPLICATION_ID}
|
applicationId: ${BOOTPAY_APPLICATION_ID}
|
||||||
privateKey: ${BOOTPAY_PRIVATE_KEY}
|
privateKey: ${BOOTPAY_PRIVATE_KEY}
|
||||||
|
|
||||||
|
apple:
|
||||||
|
iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt
|
||||||
|
iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt
|
||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
aws:
|
aws:
|
||||||
credentials:
|
credentials:
|
||||||
|
|
|
@ -5,6 +5,10 @@ logging:
|
||||||
util:
|
util:
|
||||||
EC2MetadataUtils: error
|
EC2MetadataUtils: error
|
||||||
|
|
||||||
|
apple:
|
||||||
|
iapVerifyUrl: https://buy.itunes.apple.com/verifyReceipt
|
||||||
|
iapVerifySandboxUrl: https://sandbox.itunes.apple.com/verifyReceipt
|
||||||
|
|
||||||
cloud:
|
cloud:
|
||||||
aws:
|
aws:
|
||||||
credentials:
|
credentials:
|
||||||
|
|
Loading…
Reference in New Issue