Merge pull request '캔 쿠폰 시스템' (#107) from test into main
Reviewed-on: #107
This commit is contained in:
		| @@ -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") | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								src/main/kotlin/kr/co/vividnext/sodalive/aws/sqs/SqsEvent.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/main/kotlin/kr/co/vividnext/sodalive/aws/sqs/SqsEvent.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -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: | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user