From d5cc28e50b08c0c3bebac0d49dc3e7c7cab10da8 Mon Sep 17 00:00:00 2001
From: Klaus <klaus@vividnext.co.kr>
Date: Tue, 19 Sep 2023 12:15:57 +0900
Subject: [PATCH] =?UTF-8?q?point=20click=20postback=20api=20=EC=B6=94?=
 =?UTF-8?q?=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../vividnext/sodalive/can/charge/Charge.kt   |  2 +-
 .../sodalive/can/charge/ChargeStatus.kt       |  5 +-
 .../sodalive/can/charge/free/AdsCharge.kt     | 23 +++++
 .../can/charge/free/AdsChargeController.kt    | 35 ++++++++
 .../can/charge/free/AdsChargeRepository.kt    |  7 ++
 .../can/charge/free/AdsChargeService.kt       | 85 +++++++++++++++++++
 .../sodalive/can/payment/PaymentGateway.kt    |  2 +-
 .../sodalive/common/SodaException.kt          |  2 +
 .../sodalive/common/SodaExceptionHandler.kt   |  9 ++
 .../sodalive/configs/SecurityConfig.kt        |  1 +
 src/main/resources/application.yml            |  5 ++
 src/test/resources/application.yml            |  3 +
 12 files changed, 176 insertions(+), 3 deletions(-)
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsCharge.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsChargeController.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsChargeRepository.kt
 create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsChargeService.kt

diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/Charge.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/Charge.kt
index bb59e85..f791e86 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/Charge.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/Charge.kt
@@ -21,7 +21,7 @@ data class Charge(
     @Enumerated(value = EnumType.STRING)
     var status: ChargeStatus = ChargeStatus.CHARGE
 ) : BaseEntity() {
-    @OneToOne(fetch = FetchType.LAZY)
+    @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "can_id", nullable = true)
     var can: Can? = null
 
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeStatus.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeStatus.kt
index 07d5c0f..1585dcd 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeStatus.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeStatus.kt
@@ -4,5 +4,8 @@ enum class ChargeStatus {
     CHARGE, REFUND_CHARGE, EVENT, CANCEL,
 
     // 관리자 지급
-    ADMIN
+    ADMIN,
+
+    // 광고
+    ADS
 }
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsCharge.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsCharge.kt
new file mode 100644
index 0000000..a0b0ffe
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsCharge.kt
@@ -0,0 +1,23 @@
+package kr.co.vividnext.sodalive.can.charge.free
+
+import kr.co.vividnext.sodalive.common.BaseEntity
+import kr.co.vividnext.sodalive.member.Member
+import javax.persistence.Entity
+import javax.persistence.FetchType
+import javax.persistence.JoinColumn
+import javax.persistence.ManyToOne
+
+@Entity
+data class AdsCharge(
+    val transactionKey: String,
+    val adKey: String,
+    val adName: String,
+    val adProfit: Int,
+    val adCurrency: String,
+    val point: Int,
+    val deviceIfa: String
+) : BaseEntity() {
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "member_id", nullable = false)
+    var member: Member? = null
+}
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsChargeController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsChargeController.kt
new file mode 100644
index 0000000..ea6bf41
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsChargeController.kt
@@ -0,0 +1,35 @@
+package kr.co.vividnext.sodalive.can.charge.free
+
+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("/charge/ads")
+class AdsChargeController(private val service: AdsChargeService) {
+    @GetMapping
+    fun adsCharge(
+        @RequestParam("transaction_key") transactionKey: String,
+        @RequestParam("placement_uid") placementUid: String,
+        @RequestParam("ad_key") adKey: String,
+        @RequestParam("ad_name") adName: String,
+        @RequestParam("ad_profit") adProfit: String,
+        @RequestParam("ad_currency") adCurrency: String,
+        @RequestParam("point") point: String,
+        @RequestParam("device_ifa") deviceIfa: String,
+        @RequestParam("picker_uid") memberId: String
+    ) {
+        service.adsCharge(
+            transactionKey,
+            placementUid,
+            adKey,
+            adName,
+            adProfit,
+            adCurrency,
+            point,
+            deviceIfa,
+            memberId
+        )
+    }
+}
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsChargeRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsChargeRepository.kt
new file mode 100644
index 0000000..8b71aea
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsChargeRepository.kt
@@ -0,0 +1,7 @@
+package kr.co.vividnext.sodalive.can.charge.free
+
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface AdsChargeRepository : JpaRepository<AdsCharge, Long>
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsChargeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsChargeService.kt
new file mode 100644
index 0000000..f58b257
--- /dev/null
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/free/AdsChargeService.kt
@@ -0,0 +1,85 @@
+package kr.co.vividnext.sodalive.can.charge.free
+
+import kr.co.vividnext.sodalive.can.charge.Charge
+import kr.co.vividnext.sodalive.can.charge.ChargeRepository
+import kr.co.vividnext.sodalive.can.charge.ChargeStatus
+import kr.co.vividnext.sodalive.can.payment.Payment
+import kr.co.vividnext.sodalive.can.payment.PaymentGateway
+import kr.co.vividnext.sodalive.can.payment.PaymentStatus
+import kr.co.vividnext.sodalive.common.AdsChargeException
+import kr.co.vividnext.sodalive.fcm.FcmEvent
+import kr.co.vividnext.sodalive.fcm.FcmEventType
+import kr.co.vividnext.sodalive.member.MemberRepository
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.ApplicationEventPublisher
+import org.springframework.data.repository.findByIdOrNull
+import org.springframework.stereotype.Service
+import org.springframework.transaction.annotation.Transactional
+
+@Service
+class AdsChargeService(
+    private val repository: AdsChargeRepository,
+    private val memberRepository: MemberRepository,
+    private val chargeRepository: ChargeRepository,
+    private val applicationEventPublisher: ApplicationEventPublisher,
+
+    @Value("\${point-click.placement-uid}")
+    private val placementUid: String
+) {
+    @Transactional
+    fun adsCharge(
+        transactionKey: String,
+        placementUid: String,
+        adKey: String,
+        adName: String,
+        adProfit: String,
+        adCurrency: String,
+        point: String,
+        deviceIfa: String,
+        memberId: String
+    ) {
+        if (placementUid != this.placementUid) {
+            throw AdsChargeException("잘못된 요청입니다.")
+        }
+
+        val member = memberRepository.findByIdOrNull(id = memberId.toLong())
+            ?: throw AdsChargeException("잘못된 요청입니다.")
+
+        val adsCharge = AdsCharge(
+            transactionKey = transactionKey,
+            adKey = adKey,
+            adName = adName,
+            adProfit = adProfit.toInt(),
+            adCurrency = adCurrency,
+            point = point.toInt(),
+            deviceIfa = deviceIfa
+        )
+        adsCharge.member = member
+        repository.save(adsCharge)
+
+        val charge = Charge(0, rewardCan = point.toInt(), status = ChargeStatus.ADS)
+        charge.title = "${point.toInt()} 캔"
+        charge.member = member
+
+        val payment = Payment(
+            status = PaymentStatus.COMPLETE,
+            paymentGateway = PaymentGateway.POINT_CLICK_AD
+        )
+
+        payment.method = "제휴보상"
+        charge.payment = payment
+        chargeRepository.save(charge)
+
+        member.charge(0, point.toInt(), "ads")
+
+        applicationEventPublisher.publishEvent(
+            FcmEvent(
+                type = FcmEventType.INDIVIDUAL,
+                title = "제휴보상",
+                message = "${point.toInt()} 캔이 지급되었습니다.",
+                recipients = listOf(member.id!!),
+                isAuth = false
+            )
+        )
+    }
+}
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/PaymentGateway.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/PaymentGateway.kt
index 8874c09..a37459d 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/PaymentGateway.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/payment/PaymentGateway.kt
@@ -1,5 +1,5 @@
 package kr.co.vividnext.sodalive.can.payment
 
 enum class PaymentGateway {
-    PG, GOOGLE_IAP, APPLE_IAP
+    PG, GOOGLE_IAP, APPLE_IAP, POINT_CLICK_AD
 }
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt
index cf3cb9e..261e481 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt
@@ -1,3 +1,5 @@
 package kr.co.vividnext.sodalive.common
 
 class SodaException(message: String, val errorProperty: String? = null) : RuntimeException(message)
+
+class AdsChargeException(message: String) : RuntimeException(message)
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt b/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt
index 65c1599..045cf83 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt
@@ -2,10 +2,12 @@ package kr.co.vividnext.sodalive.common
 
 import org.slf4j.LoggerFactory
 import org.springframework.dao.DataIntegrityViolationException
+import org.springframework.http.HttpStatus
 import org.springframework.security.access.AccessDeniedException
 import org.springframework.security.authentication.BadCredentialsException
 import org.springframework.security.authentication.InternalAuthenticationServiceException
 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
 
@@ -52,6 +54,13 @@ class SodaExceptionHandler {
         ApiResponse.error("이미 등록되어 있습니다.")
     }
 
+    @ResponseStatus(value = HttpStatus.NOT_FOUND)
+    @ExceptionHandler(AdsChargeException::class)
+    fun handleAdsChargeException(e: AdsChargeException) = run {
+        logger.error("API error - AdsChargeException ::: ", e)
+        ApiResponse.error("잘못된 요청입니다.")
+    }
+
     @ExceptionHandler(Exception::class)
     fun handleException(e: Exception) = run {
         logger.error("API error", e)
diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt
index b597a5c..7c8a24e 100644
--- a/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt
+++ b/src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt
@@ -72,6 +72,7 @@ class SecurityConfig(
             .antMatchers("/member/forgot-password").permitAll()
             .antMatchers("/stplat/terms_of_service").permitAll()
             .antMatchers("/stplat/privacy_policy").permitAll()
+            .antMatchers("/charge/ads").permitAll()
             .anyRequest().authenticated()
             .and()
             .build()
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 1941134..4dd8bb8 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -20,6 +20,9 @@ agora:
     appId: ${AGORA_APP_ID}
     appCertificate: ${AGORA_APP_CERTIFICATE}
 
+pointClick:
+    placementUid: fc07cfb1-ef16-455c-bdad-22aa9e8fd78c
+
 firebase:
     secretKeyPath: ${GOOGLE_APPLICATION_CREDENTIALS}
 
@@ -88,3 +91,5 @@ spring:
                 show_sql: true
                 format_sql: true
 
+pointClick:
+    placementUid: test
diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml
index 3fc8d6c..c831d44 100644
--- a/src/test/resources/application.yml
+++ b/src/test/resources/application.yml
@@ -13,6 +13,9 @@ agora:
     appId: ${AGORA_APP_ID}
     appCertificate: ${AGORA_APP_CERTIFICATE}
 
+pointClick:
+    placementUid: test
+
 cloud:
     aws:
         credentials: