Merge pull request '캔 쿠폰 시스템' (#107) from test into main
Reviewed-on: #107
This commit is contained in:
commit
9ff6ec1888
|
@ -44,6 +44,7 @@ dependencies {
|
||||||
kapt("org.springframework.boot:spring-boot-configuration-processor")
|
kapt("org.springframework.boot:spring-boot-configuration-processor")
|
||||||
|
|
||||||
// aws
|
// aws
|
||||||
|
implementation("com.amazonaws:aws-java-sdk-sqs:1.12.380")
|
||||||
implementation("com.amazonaws:aws-java-sdk-ses:1.12.380")
|
implementation("com.amazonaws:aws-java-sdk-ses:1.12.380")
|
||||||
implementation("com.amazonaws:aws-java-sdk-s3:1.12.380")
|
implementation("com.amazonaws:aws-java-sdk-s3:1.12.380")
|
||||||
implementation("com.amazonaws:aws-java-sdk-cloudfront:1.12.380")
|
implementation("com.amazonaws:aws-java-sdk-cloudfront:1.12.380")
|
||||||
|
@ -58,6 +59,8 @@ dependencies {
|
||||||
// firebase admin sdk
|
// firebase admin sdk
|
||||||
implementation("com.google.firebase:firebase-admin:9.2.0")
|
implementation("com.google.firebase:firebase-admin:9.2.0")
|
||||||
|
|
||||||
|
implementation("org.apache.poi:poi-ooxml:5.2.3")
|
||||||
|
|
||||||
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,25 @@
|
||||||
|
package kr.co.vividnext.sodalive.aws.sqs
|
||||||
|
|
||||||
|
import org.springframework.context.event.EventListener
|
||||||
|
import org.springframework.scheduling.annotation.Async
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
|
||||||
|
enum class SqsEventType {
|
||||||
|
GENERATE_COUPON
|
||||||
|
}
|
||||||
|
|
||||||
|
class SqsEvent(
|
||||||
|
val type: SqsEventType,
|
||||||
|
val message: String
|
||||||
|
)
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class SqsEventListener(private val sqsService: SqsService) {
|
||||||
|
@Async
|
||||||
|
@EventListener
|
||||||
|
fun sendMessage(sqsEvent: SqsEvent) {
|
||||||
|
when (sqsEvent.type) {
|
||||||
|
SqsEventType.GENERATE_COUPON -> sqsService.sendGenerateCouponMessage(sqsEvent.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package kr.co.vividnext.sodalive.aws.sqs
|
||||||
|
|
||||||
|
import com.amazonaws.services.sqs.AmazonSQS
|
||||||
|
import com.amazonaws.services.sqs.model.SendMessageRequest
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class SqsService(
|
||||||
|
private val amazonSQS: AmazonSQS,
|
||||||
|
|
||||||
|
@Value("\${cloud.aws.sqs.generate-coupon-url}")
|
||||||
|
private val generateCouponQueueUrl: String
|
||||||
|
) {
|
||||||
|
fun sendGenerateCouponMessage(message: String) {
|
||||||
|
val request = SendMessageRequest()
|
||||||
|
.withQueueUrl(generateCouponQueueUrl)
|
||||||
|
.withMessageBody(message)
|
||||||
|
|
||||||
|
amazonSQS.sendMessage(request)
|
||||||
|
}
|
||||||
|
}
|
|
@ -111,6 +111,10 @@ class CanService(private val repository: CanRepository) {
|
||||||
"제휴보상"
|
"제휴보상"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ChargeStatus.COUPON -> {
|
||||||
|
it.payment!!.method ?: "쿠폰충전"
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
"환불"
|
"환불"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import kr.co.bootpay.Bootpay
|
import kr.co.bootpay.Bootpay
|
||||||
import kr.co.vividnext.sodalive.can.CanRepository
|
import kr.co.vividnext.sodalive.can.CanRepository
|
||||||
import kr.co.vividnext.sodalive.can.charge.event.ChargeSpringEvent
|
import kr.co.vividnext.sodalive.can.charge.event.ChargeSpringEvent
|
||||||
|
import kr.co.vividnext.sodalive.can.coupon.CanCouponNumberRepository
|
||||||
import kr.co.vividnext.sodalive.can.payment.Payment
|
import kr.co.vividnext.sodalive.can.payment.Payment
|
||||||
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||||
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
|
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
|
||||||
|
@ -29,6 +30,8 @@ class ChargeService(
|
||||||
private val chargeRepository: ChargeRepository,
|
private val chargeRepository: ChargeRepository,
|
||||||
private val canRepository: CanRepository,
|
private val canRepository: CanRepository,
|
||||||
private val memberRepository: MemberRepository,
|
private val memberRepository: MemberRepository,
|
||||||
|
private val couponNumberRepository: CanCouponNumberRepository,
|
||||||
|
|
||||||
private val objectMapper: ObjectMapper,
|
private val objectMapper: ObjectMapper,
|
||||||
private val okHttpClient: OkHttpClient,
|
private val okHttpClient: OkHttpClient,
|
||||||
private val applicationEventPublisher: ApplicationEventPublisher,
|
private val applicationEventPublisher: ApplicationEventPublisher,
|
||||||
|
@ -43,6 +46,33 @@ class ChargeService(
|
||||||
private val appleInAppVerifyUrl: String
|
private val appleInAppVerifyUrl: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun chargeByCoupon(couponNumber: String, member: Member) {
|
||||||
|
val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber)
|
||||||
|
?: throw SodaException("잘못된 쿠폰번호입니다.\n고객센터로 문의해 주시기 바랍니다.")
|
||||||
|
|
||||||
|
if (canCouponNumber.member != null) {
|
||||||
|
throw SodaException("이미 사용한 쿠폰번호 입니다.")
|
||||||
|
}
|
||||||
|
|
||||||
|
canCouponNumber.member = member
|
||||||
|
|
||||||
|
val coupon = canCouponNumber.canCoupon!!
|
||||||
|
val couponCharge = Charge(0, coupon.can, status = ChargeStatus.COUPON)
|
||||||
|
couponCharge.title = "${coupon.can} 캔"
|
||||||
|
couponCharge.member = member
|
||||||
|
|
||||||
|
val payment = Payment(
|
||||||
|
status = PaymentStatus.COMPLETE,
|
||||||
|
paymentGateway = PaymentGateway.PG
|
||||||
|
)
|
||||||
|
payment.method = coupon.couponName
|
||||||
|
couponCharge.payment = payment
|
||||||
|
chargeRepository.save(couponCharge)
|
||||||
|
|
||||||
|
member.charge(0, coupon.can, "pg")
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun charge(member: Member, request: ChargeRequest): ChargeResponse {
|
fun charge(member: Member, request: ChargeRequest): ChargeResponse {
|
||||||
val can = canRepository.findByIdOrNull(request.canId)
|
val can = canRepository.findByIdOrNull(request.canId)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package kr.co.vividnext.sodalive.can.charge
|
package kr.co.vividnext.sodalive.can.charge
|
||||||
|
|
||||||
enum class ChargeStatus {
|
enum class ChargeStatus {
|
||||||
CHARGE, REFUND_CHARGE, EVENT, CANCEL,
|
CHARGE, REFUND_CHARGE, EVENT, COUPON, CANCEL,
|
||||||
|
|
||||||
// 관리자 지급
|
// 관리자 지급
|
||||||
ADMIN,
|
ADMIN,
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.coupon
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import javax.persistence.Entity
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class CanCoupon(
|
||||||
|
val couponName: String,
|
||||||
|
val can: Int,
|
||||||
|
val couponCount: Int,
|
||||||
|
val validity: LocalDateTime,
|
||||||
|
val isActive: Boolean,
|
||||||
|
val isMultipleUse: Boolean
|
||||||
|
) : BaseEntity()
|
|
@ -0,0 +1,107 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.coupon
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import org.springframework.core.io.InputStreamResource
|
||||||
|
import org.springframework.data.domain.Pageable
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/can/coupon")
|
||||||
|
class CanCouponController(private val service: CanCouponService) {
|
||||||
|
@PostMapping
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
fun generateCoupon(
|
||||||
|
@RequestBody request: GenerateCanCouponRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.generateCoupon(request))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
fun getCouponList(
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||||
|
pageable: Pageable
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(service.getCouponList(offset = pageable.offset, limit = pageable.pageSize.toLong()))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/number-list")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
fun getCouponNumberList(
|
||||||
|
@RequestParam couponId: Long,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||||
|
pageable: Pageable
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(
|
||||||
|
service.getCouponNumberList(
|
||||||
|
couponId = couponId,
|
||||||
|
offset = pageable.offset,
|
||||||
|
limit = pageable.pageSize.toLong()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/number-list/download")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
fun downloadCouponNumberList(
|
||||||
|
@RequestParam couponId: Long,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
val fileName = "쿠폰번호리스트.xlsx"
|
||||||
|
val encodedFileName = URLEncoder.encode(
|
||||||
|
fileName,
|
||||||
|
StandardCharsets.UTF_8.toString()
|
||||||
|
).replace("+", "%20")
|
||||||
|
val contentDisposition = "attachment; filename*=UTF-8''$encodedFileName"
|
||||||
|
val headers = HttpHeaders().apply {
|
||||||
|
add(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
|
||||||
|
}
|
||||||
|
|
||||||
|
val response = service.downloadCouponNumberList(couponId)
|
||||||
|
ResponseEntity.ok()
|
||||||
|
.headers(headers)
|
||||||
|
.contentType(
|
||||||
|
MediaType
|
||||||
|
.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
)
|
||||||
|
.body(InputStreamResource(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/use")
|
||||||
|
fun useCanCoupon(
|
||||||
|
@RequestBody request: UseCanCouponRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
ApiResponse.ok(
|
||||||
|
service.useCanCoupon(
|
||||||
|
couponNumber = request.couponNumber,
|
||||||
|
memberId = member.id!!
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.coupon
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class CanCouponIssueService(private val couponNumberRepository: CanCouponNumberRepository) {
|
||||||
|
fun validateAvailableUseCoupon(couponNumber: String, memberId: Long) {
|
||||||
|
val canCouponNumber = checkCanCouponNumber(couponNumber)
|
||||||
|
|
||||||
|
if (!isMultipleUse(canCouponNumber)) {
|
||||||
|
val canCouponNumberList = couponNumberRepository.findByMemberId(memberId = memberId)
|
||||||
|
if (canCouponNumberList.isNotEmpty()) {
|
||||||
|
throw SodaException("해당 쿠폰은 1회만 충전이 가능합니다.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkCanCouponNumber(couponNumber: String): CanCouponNumber {
|
||||||
|
val canCouponNumber = couponNumberRepository.findByCouponNumber(couponNumber = couponNumber)
|
||||||
|
?: throw SodaException("잘못된 쿠폰번호입니다.\n고객센터로 문의해 주시기 바랍니다.")
|
||||||
|
|
||||||
|
if (canCouponNumber.member != null) {
|
||||||
|
throw SodaException("이미 사용한 쿠폰번호 입니다.")
|
||||||
|
}
|
||||||
|
|
||||||
|
return canCouponNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isMultipleUse(canCouponNumber: CanCouponNumber) = canCouponNumber.canCoupon!!.isMultipleUse
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.coupon
|
||||||
|
|
||||||
|
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
|
||||||
|
import javax.persistence.OneToOne
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
data class CanCouponNumber(
|
||||||
|
val couponNumber: String
|
||||||
|
) : BaseEntity() {
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "can_coupon_id", nullable = false)
|
||||||
|
var canCoupon: CanCoupon? = null
|
||||||
|
|
||||||
|
@OneToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "member_id", nullable = true)
|
||||||
|
var member: Member? = null
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.coupon
|
||||||
|
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
|
import kr.co.vividnext.sodalive.can.coupon.QCanCouponNumber.canCouponNumber
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
|
||||||
|
interface CanCouponNumberRepository : JpaRepository<CanCouponNumber, Long>, CanCouponNumberQueryRepository
|
||||||
|
|
||||||
|
interface CanCouponNumberQueryRepository {
|
||||||
|
fun getUseCouponCount(id: Long): Int
|
||||||
|
|
||||||
|
fun getCouponNumberTotalCount(couponId: Long): Int
|
||||||
|
|
||||||
|
fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List<GetCouponNumberListItemResponse>
|
||||||
|
|
||||||
|
fun getAllCouponNumberList(couponId: Long): List<GetCouponNumberListItemResponse>
|
||||||
|
|
||||||
|
fun findByCouponNumber(couponNumber: String): CanCouponNumber?
|
||||||
|
|
||||||
|
fun findByMemberId(memberId: Long): List<Long>
|
||||||
|
}
|
||||||
|
|
||||||
|
class CanCouponNumberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponNumberQueryRepository {
|
||||||
|
override fun getUseCouponCount(id: Long): Int {
|
||||||
|
return queryFactory
|
||||||
|
.select(canCouponNumber.id)
|
||||||
|
.from(canCouponNumber)
|
||||||
|
.where(canCouponNumber.member.isNotNull)
|
||||||
|
.fetch()
|
||||||
|
.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCouponNumberTotalCount(couponId: Long): Int {
|
||||||
|
return queryFactory
|
||||||
|
.select(canCouponNumber.id)
|
||||||
|
.from(canCouponNumber)
|
||||||
|
.where(canCouponNumber.canCoupon.id.eq(couponId))
|
||||||
|
.fetch()
|
||||||
|
.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): List<GetCouponNumberListItemResponse> {
|
||||||
|
return queryFactory
|
||||||
|
.select(
|
||||||
|
QGetCouponNumberListItemResponse(
|
||||||
|
canCouponNumber.id,
|
||||||
|
canCouponNumber.couponNumber,
|
||||||
|
canCouponNumber.member.isNotNull
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(canCouponNumber)
|
||||||
|
.where(canCouponNumber.canCoupon.id.eq(couponId))
|
||||||
|
.orderBy(canCouponNumber.id.asc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAllCouponNumberList(couponId: Long): List<GetCouponNumberListItemResponse> {
|
||||||
|
return queryFactory
|
||||||
|
.select(
|
||||||
|
QGetCouponNumberListItemResponse(
|
||||||
|
canCouponNumber.id,
|
||||||
|
canCouponNumber.couponNumber,
|
||||||
|
canCouponNumber.member.isNotNull
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.from(canCouponNumber)
|
||||||
|
.where(canCouponNumber.canCoupon.id.eq(couponId))
|
||||||
|
.orderBy(canCouponNumber.id.asc())
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findByCouponNumber(couponNumber: String): CanCouponNumber? {
|
||||||
|
return queryFactory
|
||||||
|
.selectFrom(canCouponNumber)
|
||||||
|
.where(canCouponNumber.couponNumber.eq(couponNumber))
|
||||||
|
.fetchFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findByMemberId(memberId: Long): List<Long> {
|
||||||
|
return queryFactory
|
||||||
|
.select(canCouponNumber.id)
|
||||||
|
.from(canCouponNumber)
|
||||||
|
.where(canCouponNumber.member.id.eq(memberId))
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.coupon
|
||||||
|
|
||||||
|
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||||
|
import kr.co.vividnext.sodalive.can.coupon.QCanCoupon.canCoupon
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
|
||||||
|
interface CanCouponRepository : JpaRepository<CanCoupon, Long>, CanCouponQueryRepository
|
||||||
|
|
||||||
|
interface CanCouponQueryRepository {
|
||||||
|
fun getCouponTotalCount(): Int
|
||||||
|
fun getCouponList(offset: Long, limit: Long): List<CanCoupon>
|
||||||
|
}
|
||||||
|
|
||||||
|
class CanCouponQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CanCouponQueryRepository {
|
||||||
|
override fun getCouponTotalCount(): Int {
|
||||||
|
return queryFactory
|
||||||
|
.select(canCoupon.id)
|
||||||
|
.from(canCoupon)
|
||||||
|
.fetch()
|
||||||
|
.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCouponList(offset: Long, limit: Long): List<CanCoupon> {
|
||||||
|
return queryFactory
|
||||||
|
.selectFrom(canCoupon)
|
||||||
|
.orderBy(canCoupon.id.desc())
|
||||||
|
.offset(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.fetch()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.coupon
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import kr.co.vividnext.sodalive.aws.sqs.SqsEvent
|
||||||
|
import kr.co.vividnext.sodalive.aws.sqs.SqsEventType
|
||||||
|
import kr.co.vividnext.sodalive.can.charge.ChargeService
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRepository
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
||||||
|
import org.springframework.context.ApplicationEventPublisher
|
||||||
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class CanCouponService(
|
||||||
|
private val issueService: CanCouponIssueService,
|
||||||
|
private val chargeService: ChargeService,
|
||||||
|
|
||||||
|
private val repository: CanCouponRepository,
|
||||||
|
private val couponNumberRepository: CanCouponNumberRepository,
|
||||||
|
|
||||||
|
private val memberRepository: MemberRepository,
|
||||||
|
|
||||||
|
private val objectMapper: ObjectMapper,
|
||||||
|
private val applicationEventPublisher: ApplicationEventPublisher
|
||||||
|
) {
|
||||||
|
fun generateCoupon(request: GenerateCanCouponRequest) {
|
||||||
|
val message = objectMapper.writeValueAsString(request)
|
||||||
|
applicationEventPublisher.publishEvent(SqsEvent(type = SqsEventType.GENERATE_COUPON, message = message))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCouponList(offset: Long, limit: Long): GetCouponListResponse {
|
||||||
|
val totalCount = repository.getCouponTotalCount()
|
||||||
|
|
||||||
|
val items = repository.getCouponList(offset = offset, limit = limit)
|
||||||
|
.asSequence()
|
||||||
|
.map {
|
||||||
|
val useCouponCount = couponNumberRepository.getUseCouponCount(id = it.id!!)
|
||||||
|
GetCouponListItemResponse(
|
||||||
|
id = it.id!!,
|
||||||
|
couponName = it.couponName,
|
||||||
|
can = it.can,
|
||||||
|
couponCount = it.couponCount,
|
||||||
|
useCouponCount = useCouponCount,
|
||||||
|
validity = it.validity.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
|
||||||
|
isMultipleUse = it.isMultipleUse,
|
||||||
|
isActive = it.isActive
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
return GetCouponListResponse(totalCount, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCouponNumberList(couponId: Long, offset: Long, limit: Long): GetCouponNumberListResponse {
|
||||||
|
val totalCount = couponNumberRepository.getCouponNumberTotalCount(couponId = couponId)
|
||||||
|
val items = couponNumberRepository.getCouponNumberList(couponId = couponId, offset = offset, limit = limit)
|
||||||
|
return GetCouponNumberListResponse(totalCount, items)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadCouponNumberList(couponId: Long): ByteArrayInputStream {
|
||||||
|
val header = listOf("순번", "쿠폰번호", "사용여부")
|
||||||
|
val byteArrayOutputStream = ByteArrayOutputStream()
|
||||||
|
val couponNumberList = couponNumberRepository.getAllCouponNumberList(couponId)
|
||||||
|
|
||||||
|
val workbook = XSSFWorkbook()
|
||||||
|
try {
|
||||||
|
val sheet = workbook.createSheet()
|
||||||
|
val row = sheet.createRow(0)
|
||||||
|
header.forEachIndexed { index, string ->
|
||||||
|
val cell = row.createCell(index)
|
||||||
|
cell.setCellValue(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
couponNumberList.forEachIndexed { index, item ->
|
||||||
|
val couponNumberRow = sheet.createRow(index + 1)
|
||||||
|
couponNumberRow.createCell(0).setCellValue(item.couponNumberId.toString())
|
||||||
|
couponNumberRow.createCell(1).setCellValue(insertHyphens(item.couponNumber))
|
||||||
|
couponNumberRow.createCell(2).setCellValue(
|
||||||
|
if (item.isUsed) {
|
||||||
|
"O"
|
||||||
|
} else {
|
||||||
|
"X"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
workbook.write(byteArrayOutputStream)
|
||||||
|
return ByteArrayInputStream(byteArrayOutputStream.toByteArray())
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw SodaException("다운로드를 하지 못했습니다.\n다시 시도해 주세요.")
|
||||||
|
} finally {
|
||||||
|
workbook.close()
|
||||||
|
byteArrayOutputStream.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun useCanCoupon(couponNumber: String, memberId: Long) {
|
||||||
|
val member = memberRepository.findByIdOrNull(id = memberId)
|
||||||
|
?: throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
|
||||||
|
if (member.auth == null) throw SodaException("쿠폰은 본인인증을 하셔야 사용이 가능합니다.")
|
||||||
|
|
||||||
|
issueService.validateAvailableUseCoupon(couponNumber, memberId)
|
||||||
|
|
||||||
|
chargeService.chargeByCoupon(couponNumber, member)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun insertHyphens(input: String): String {
|
||||||
|
return input.chunked(4).joinToString("-")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.coupon
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
|
||||||
|
data class GenerateCanCouponRequest(
|
||||||
|
@JsonProperty("couponName") val couponName: String,
|
||||||
|
@JsonProperty("can") val can: Int,
|
||||||
|
@JsonProperty("validity") val validity: String,
|
||||||
|
@JsonProperty("isMultipleUse") val isMultipleUse: Boolean,
|
||||||
|
@JsonProperty("couponNumberCount") val couponNumberCount: Int
|
||||||
|
)
|
|
@ -0,0 +1,17 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.coupon
|
||||||
|
|
||||||
|
data class GetCouponListResponse(
|
||||||
|
val totalCount: Int,
|
||||||
|
val items: List<GetCouponListItemResponse>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetCouponListItemResponse(
|
||||||
|
val id: Long,
|
||||||
|
val couponName: String,
|
||||||
|
val can: Int,
|
||||||
|
val couponCount: Int,
|
||||||
|
val useCouponCount: Int,
|
||||||
|
val validity: String,
|
||||||
|
val isMultipleUse: Boolean,
|
||||||
|
val isActive: Boolean
|
||||||
|
)
|
|
@ -0,0 +1,14 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.coupon
|
||||||
|
|
||||||
|
import com.querydsl.core.annotations.QueryProjection
|
||||||
|
|
||||||
|
data class GetCouponNumberListResponse(
|
||||||
|
val totalCount: Int,
|
||||||
|
val items: List<GetCouponNumberListItemResponse>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GetCouponNumberListItemResponse @QueryProjection constructor(
|
||||||
|
val couponNumberId: Long,
|
||||||
|
val couponNumber: String,
|
||||||
|
val isUsed: Boolean
|
||||||
|
)
|
|
@ -0,0 +1,3 @@
|
||||||
|
package kr.co.vividnext.sodalive.can.coupon
|
||||||
|
|
||||||
|
data class UseCanCouponRequest(val couponNumber: String)
|
|
@ -0,0 +1,30 @@
|
||||||
|
package kr.co.vividnext.sodalive.configs
|
||||||
|
|
||||||
|
import com.amazonaws.auth.AWSStaticCredentialsProvider
|
||||||
|
import com.amazonaws.auth.BasicAWSCredentials
|
||||||
|
import com.amazonaws.services.sqs.AmazonSQS
|
||||||
|
import com.amazonaws.services.sqs.AmazonSQSClientBuilder
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class AmazonSQSConfig(
|
||||||
|
@Value("\${cloud.aws.credentials.access-key}")
|
||||||
|
private val accessKey: String,
|
||||||
|
@Value("\${cloud.aws.credentials.secret-key}")
|
||||||
|
private val secretKey: String,
|
||||||
|
@Value("\${cloud.aws.region.static}")
|
||||||
|
private val region: String
|
||||||
|
) {
|
||||||
|
@Bean
|
||||||
|
fun amazonSQS(): AmazonSQS {
|
||||||
|
val basicAWSCredentials = BasicAWSCredentials(accessKey, secretKey)
|
||||||
|
val awsStaticCredentialsProvider = AWSStaticCredentialsProvider(basicAWSCredentials)
|
||||||
|
|
||||||
|
return AmazonSQSClientBuilder.standard()
|
||||||
|
.withCredentials(awsStaticCredentialsProvider)
|
||||||
|
.withRegion(region)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,6 +37,8 @@ cloud:
|
||||||
keyPairId: ${CONTENT_CLOUD_FRONT_KEY_PAIR_ID}
|
keyPairId: ${CONTENT_CLOUD_FRONT_KEY_PAIR_ID}
|
||||||
cloudFront:
|
cloudFront:
|
||||||
host: ${CLOUD_FRONT_HOST}
|
host: ${CLOUD_FRONT_HOST}
|
||||||
|
sqs:
|
||||||
|
generateCouponUrl: ${AMAZON_SQS_GENERATE_CAN_COUPON_QUEUE_URL}
|
||||||
region:
|
region:
|
||||||
static: ap-northeast-2
|
static: ap-northeast-2
|
||||||
stack:
|
stack:
|
||||||
|
|
Loading…
Reference in New Issue