Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

쿠폰 선물할 메시지를 전달받는다. #21

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.example.estdelivery.coupon.application

import com.example.estdelivery.coupon.application.port.`in`.GiftCouponByMessageUseCase
import com.example.estdelivery.coupon.application.port.out.CreateGiftCouponMessageStatePort
import com.example.estdelivery.coupon.application.port.out.LoadMemberStatePort
import com.example.estdelivery.coupon.application.port.out.UpdateMemberStatePort
import com.example.estdelivery.coupon.application.port.out.ValidateGiftCouponCodeStatePort
import com.example.estdelivery.coupon.application.utils.TransactionArea
import com.example.estdelivery.coupon.domain.coupon.Coupon
import com.example.estdelivery.coupon.domain.coupon.GiftCouponCode
import com.example.estdelivery.coupon.domain.coupon.GiftMessage
import com.example.estdelivery.coupon.domain.member.Member

class GiftCouponByMessageService(
loadMemberStatePort: LoadMemberStatePort,
createGiftCouponMessageStatePort: CreateGiftCouponMessageStatePort,
updateMemberStatePort: UpdateMemberStatePort,
validateGiftCouponCodeStatePort: ValidateGiftCouponCodeStatePort,
private val transactionArea: TransactionArea,
private val findMember: (Long) -> Member = { loadMemberStatePort.findMember(it) },
private val createGiftMessage: (Member, Coupon, String, GiftCouponCode) -> GiftMessage = { sender, coupon, message, code ->
createGiftCouponMessageStatePort.create(sender, coupon, message, code)
},
private val validateCouponCode: (GiftCouponCode) -> Boolean = { validateGiftCouponCodeStatePort.validate(it) },
private val updateMembersCoupon: (Member) -> Unit = { updateMemberStatePort.updateMembersCoupon(it) }
) : GiftCouponByMessageUseCase {
/**
* 선물 메시지를 통해 쿠폰을 선물한다.
*
* 1. 보낼 사람의 정보를 조회한다.
* 2. 보낼 쿠폰의 정보를 조회한다.
* 3. 보낼 사람의 쿠폰북에서 쿠폰을 삭제한다.
* 4. 등록할 쿠폰 코드를 검증한다.
* 5. 선물 메시지를 생성한다.
*
* @param memberId 선물하는 회원 식별자
* @param couponId 선물할 쿠폰 식별자
* @param giftMessage 선물 메시지
*/
override fun sendGiftAvailableCoupon(memberId: Long, couponId: Long, giftMessage: String): GiftMessage {
return transactionArea.run {
val sender = findMember(memberId)
val coupon = sender.showMyCouponBook().find { it.id == couponId }
if (coupon == null) {
throw IllegalArgumentException("쿠폰이 존재하지 않습니다.")
}
val couponCode: GiftCouponCode = getGiftCouponCode()
sender.useCoupon(coupon)
updateMembersCoupon(sender)
createGiftMessage(sender, coupon, giftMessage, couponCode)
}
}

private fun getGiftCouponCode(): GiftCouponCode {
lateinit var couponCode: GiftCouponCode
do {
couponCode = GiftCouponCode.create()
} while (!validateCouponCode(couponCode))
return couponCode
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
package com.example.estdelivery.coupon.application

import com.example.estdelivery.coupon.application.port.`in`.FindAvailableGiftCouponUseCase
import com.example.estdelivery.coupon.application.port.`in`.GiftCouponByMessageUseCase
import com.example.estdelivery.coupon.application.port.out.CreateGiftCouponMessageStatePort
import com.example.estdelivery.coupon.application.port.out.LoadMemberStatePort
import com.example.estdelivery.coupon.application.port.out.UpdateMemberStatePort
import com.example.estdelivery.coupon.application.port.out.ValidateGiftCouponCodeStatePort
import com.example.estdelivery.coupon.application.utils.TransactionArea
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class ServiceBeanManager {
@Bean
fun findAvailableGiftCouponService(loadMemberStatePort: LoadMemberStatePort) =
fun findAvailableGiftCouponUseCase(loadMemberStatePort: LoadMemberStatePort): FindAvailableGiftCouponUseCase =
FindAvailableGiftCouponService(loadMemberStatePort)

@Bean
fun giftCouponByMessageUseCase(
transactionArea: TransactionArea,
loadMemberStatePort: LoadMemberStatePort,
createGiftCouponMessageStatePort: CreateGiftCouponMessageStatePort,
validateGiftCouponCodeStatePort: ValidateGiftCouponCodeStatePort,
updateMemberStatePort: UpdateMemberStatePort,
): GiftCouponByMessageUseCase = GiftCouponByMessageService(
loadMemberStatePort,
createGiftCouponMessageStatePort,
updateMemberStatePort,
validateGiftCouponCodeStatePort,
transactionArea,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.estdelivery.coupon.application.port.`in`

import com.example.estdelivery.coupon.domain.coupon.GiftMessage

interface GiftCouponByMessageUseCase {
fun sendGiftAvailableCoupon(memberId: Long, couponId: Long, giftMessage: String): GiftMessage
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
package com.example.estdelivery.coupon.application.port.`in`.web

import com.example.estdelivery.coupon.application.port.`in`.FindAvailableGiftCouponUseCase
import com.example.estdelivery.coupon.application.port.`in`.GiftCouponByMessageUseCase
import com.example.estdelivery.coupon.application.port.`in`.web.dto.GiftCouponResponse
import com.example.estdelivery.coupon.application.port.`in`.web.dto.GiftCouponResponses
import com.example.estdelivery.coupon.application.port.`in`.web.dto.GiftMessageResponse
import com.example.estdelivery.coupon.domain.coupon.Coupon.FixDiscountCoupon
import com.example.estdelivery.coupon.domain.coupon.Coupon.RateDiscountCoupon
import java.net.URL
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/gift-coupons")
class GiftCouponMessageController(
private val findAvailableGiftCouponUseCase: FindAvailableGiftCouponUseCase,
private val giftCouponByMessageUseCase: GiftCouponByMessageUseCase,
) {
@GetMapping
fun findAvailableGiftCoupons(@RequestHeader(value = "Member-ID") memberId: Long): GiftCouponResponses =
Expand All @@ -29,4 +36,22 @@ class GiftCouponMessageController(
}.let {
GiftCouponResponses(it)
}

@PostMapping("/send/{couponId}")
fun sendGiftAvailableCoupon(
@RequestHeader(value = "Member-ID") memberId: Long,
@RequestParam message: String,
@PathVariable couponId: Long,
): GiftMessageResponse =
giftCouponByMessageUseCase.sendGiftAvailableCoupon(
memberId,
couponId,
message,
).let {
GiftMessageResponse(
senderName = it.sender.name,
description = it.giftMessage,
enrollHref = URL("http", "localhost", 8080, "/gift-coupons/enroll/${it.giftCouponCode.code}")
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example.estdelivery.coupon.application.port.`in`.web.dto

import java.net.URL

data class GiftMessageResponse(
val senderName: String,
val description: String,
val enrollHref: URL,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.estdelivery.coupon.application.port.out

import com.example.estdelivery.coupon.domain.coupon.Coupon
import com.example.estdelivery.coupon.domain.coupon.GiftCouponCode
import com.example.estdelivery.coupon.domain.coupon.GiftMessage
import com.example.estdelivery.coupon.domain.member.Member

interface CreateGiftCouponMessageStatePort {
fun create(sender: Member, coupon: Coupon, message: String, giftCouponCode: GiftCouponCode): GiftMessage
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.estdelivery.coupon.application.port.out

import com.example.estdelivery.coupon.domain.coupon.GiftCouponCode

interface ValidateGiftCouponCodeStatePort {
fun validate(giftCouponCode: GiftCouponCode): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.estdelivery.coupon.application.port.out.adapter.persistence

import com.example.estdelivery.coupon.application.port.out.CreateGiftCouponMessageStatePort
import com.example.estdelivery.coupon.application.port.out.ValidateGiftCouponCodeStatePort
import com.example.estdelivery.coupon.application.port.out.adapter.persistence.entity.GiftMessageEntity
import com.example.estdelivery.coupon.application.port.out.adapter.persistence.mapper.fromCoupon
import com.example.estdelivery.coupon.application.port.out.adapter.persistence.repository.GiftMessageRepository
import com.example.estdelivery.coupon.domain.coupon.Coupon
import com.example.estdelivery.coupon.domain.coupon.GiftCoupon
import com.example.estdelivery.coupon.domain.coupon.GiftCouponCode
import com.example.estdelivery.coupon.domain.coupon.GiftMessage
import com.example.estdelivery.coupon.domain.member.Member
import org.springframework.stereotype.Component

@Component
class GiftCouponMessageAdapter(
private val giftMessageRepository: GiftMessageRepository,
) : CreateGiftCouponMessageStatePort, ValidateGiftCouponCodeStatePort {
override fun create(sender: Member, coupon: Coupon, message: String, giftCouponCode: GiftCouponCode): GiftMessage {
val giftMessageEntity = giftMessageRepository.save(
GiftMessageEntity(
sender = sender.id,
coupon = fromCoupon(coupon),
message = message,
enrollCode = giftCouponCode.code
)
)
return GiftMessage(
sender = sender,
giftMessage = giftMessageEntity.message,
giftCouponCode = GiftCouponCode(giftMessageEntity.enrollCode),
giftCoupon = GiftCoupon(coupon)
)
}

override fun validate(giftCouponCode: GiftCouponCode): Boolean {
return giftMessageRepository.existsByEnrollCode(giftCouponCode.code)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.example.estdelivery.coupon.application.port.out.adapter.persistence.entity

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.OneToOne
import jakarta.persistence.Table
import java.time.LocalDate

@Entity
@Table(name = "gift_message")
class GiftMessageEntity(
val message: String,
@Column(name = "sender_id")
val sender: Long,
val enrollCode: String,
@OneToOne
@JoinColumn(name = "coupon_id")
val coupon: CouponEntity,
val enrollDate: LocalDate = LocalDate.now(),
val isUsed: Boolean = false,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is GiftMessageEntity) return false

if (id != other.id) return false

return true
}

override fun hashCode(): Int {
return id?.hashCode() ?: 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ package com.example.estdelivery.coupon.application.port.out.adapter.persistence.
import com.example.estdelivery.coupon.application.port.out.adapter.persistence.entity.CouponEntity
import org.springframework.data.jpa.repository.JpaRepository

interface CouponRepository :
JpaRepository<CouponEntity, Long>
interface CouponRepository : JpaRepository<CouponEntity, Long>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.estdelivery.coupon.application.port.out.adapter.persistence.repository

import com.example.estdelivery.coupon.application.port.out.adapter.persistence.entity.GiftMessageEntity
import org.springframework.data.jpa.repository.JpaRepository

interface GiftMessageRepository : JpaRepository<GiftMessageEntity, Long> {
fun existsByEnrollCode(enrollCode: String): Boolean
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,4 @@ package com.example.estdelivery.coupon.application.port.out.adapter.persistence.
import com.example.estdelivery.coupon.application.port.out.adapter.persistence.entity.ShopEntity
import org.springframework.data.jpa.repository.JpaRepository

interface ShopRepository :
JpaRepository<ShopEntity, Long>
interface ShopRepository : JpaRepository<ShopEntity, Long>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.estdelivery.coupon.domain.coupon

import java.util.UUID

data class GiftCouponCode(
val code: String,
) {
init {
require(code.isNotBlank()) { "쿠폰 선물 코드는 비어있을 수 없습니다." }
}

companion object {
fun create(): GiftCouponCode {
return GiftCouponCode(UUID.randomUUID().toString().replace("-", "").substring(0..9))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.estdelivery.coupon.domain.coupon

import com.example.estdelivery.coupon.domain.member.Member

data class GiftMessage(
val sender: Member,
val giftMessage: String,
val giftCouponCode: GiftCouponCode,
val giftCoupon: GiftCoupon
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.example.estdelivery.coupon.application

import com.example.estdelivery.coupon.application.port.out.CreateGiftCouponMessageStatePort
import com.example.estdelivery.coupon.application.port.out.LoadMemberStatePort
import com.example.estdelivery.coupon.application.port.out.UpdateMemberStatePort
import com.example.estdelivery.coupon.application.port.out.ValidateGiftCouponCodeStatePort
import com.example.estdelivery.coupon.application.utils.TransactionArea
import com.example.estdelivery.coupon.domain.coupon.GiftCoupon
import com.example.estdelivery.coupon.domain.coupon.GiftCouponCode
import com.example.estdelivery.coupon.domain.coupon.GiftMessage
import com.example.estdelivery.coupon.domain.fixture.나눠준_비율_할인_쿠폰
import com.example.estdelivery.coupon.domain.fixture.일건창
import com.example.estdelivery.coupon.domain.member.Member
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot

class GiftCouponByMessageServiceTest : FreeSpec({
val loadMemberStatePort = mockk<LoadMemberStatePort>()
val createGiftCouponMessageStatePort = mockk<CreateGiftCouponMessageStatePort>()
val updateMemberStatePort = mockk<UpdateMemberStatePort>()
val validateGiftCouponCodeStatePort = mockk<ValidateGiftCouponCodeStatePort>()

lateinit var giftCouponByMessageService: GiftCouponByMessageService
beforeTest {
giftCouponByMessageService = GiftCouponByMessageService(
loadMemberStatePort,
createGiftCouponMessageStatePort,
updateMemberStatePort,
validateGiftCouponCodeStatePort,
TransactionArea()
)
}

"쿠폰 선물하기 위한 메시지를 전달받는다." {
// given
val 선물할_쿠폰 = 나눠준_비율_할인_쿠폰
val 일건창 = 일건창().apply {
receiveCoupon(선물할_쿠폰)
}
val 변경된_회원_정보 = slot<Member>()
val 선물_메시지 = "선물 메시지"
val 쿠폰_코드 = GiftCouponCode.create()

// when
every { loadMemberStatePort.findMember(일건창.id) } returns 일건창
every { createGiftCouponMessageStatePort.create(일건창, 선물할_쿠폰, 선물_메시지, any()) } returns GiftMessage(
일건창,
선물_메시지,
쿠폰_코드,
GiftCoupon(선물할_쿠폰)
)
every { validateGiftCouponCodeStatePort.validate(any()) } returns true
every { updateMemberStatePort.updateMembersCoupon(capture(변경된_회원_정보)) } returns Unit

val giftAvailableCoupon =
giftCouponByMessageService.sendGiftAvailableCoupon(일건창.id, 선물할_쿠폰.id!!, 선물_메시지)

// then
변경된_회원_정보.captured.showMyCouponBook().find { it.id == 선물할_쿠폰.id } shouldBe null
giftAvailableCoupon.sender shouldBe 일건창
giftAvailableCoupon.giftCouponCode shouldBe 쿠폰_코드
}
})
Loading
Loading