코인 충전, 코인 내역 API 추가

This commit is contained in:
2023-07-29 05:37:06 +09:00
parent c06de5f9f6
commit 7c8084bdd4
25 changed files with 846 additions and 2 deletions

View File

@@ -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
}

View File

@@ -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))
}

View File

@@ -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)

View File

@@ -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>

View File

@@ -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("결제를 완료하지 못했습니다.")
}
}
}

View File

@@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.can.charge
enum class ChargeStatus {
CHARGE, REFUND_CHARGE, EVENT, CANCEL,
// 관리자 지급
ADMIN
}