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

쿠폰 코드를 입력해 쿠폰을 받는다. #22

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.example.estdelivery.coupon.application

import com.example.estdelivery.coupon.application.port.`in`.EnrollCouponByMessageUseCase
import com.example.estdelivery.coupon.application.port.out.LoadGiftCouponStatePort
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.UseGiftCouponCodeStatePort
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.member.Member


class EnrollCouponByMessageService(
loadMemberStatePort: LoadMemberStatePort,
useGiftCouponCodeStatePort: UseGiftCouponCodeStatePort,
loadGiftCouponStatePort: LoadGiftCouponStatePort,
updateMemberStatePort: UpdateMemberStatePort,
private val transactionArea: TransactionArea,
private val findMember: (Long) -> Member = { loadMemberStatePort.findMember(it) },
private val useGiftCouponCode: (GiftCouponCode) -> GiftCoupon = {
val giftCoupon = loadGiftCouponStatePort.findGiftCoupon(it)
if (giftCoupon.isUsed) throw IllegalArgumentException("이미 사용된 쿠폰입니다.")
useGiftCouponCodeStatePort.useBy(it)
giftCoupon
},
private val updateMembersCoupon: (Member) -> Unit = { updateMemberStatePort.updateMembersCoupon(it) },
) : EnrollCouponByMessageUseCase {
override fun enroll(memberId: Long, code: GiftCouponCode) {
transactionArea.run {
val member = findMember(memberId)
val giftCoupon = useGiftCouponCode(code)
member.receiveCoupon(giftCoupon.coupon)
updateMembersCoupon(member)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ 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.LoadGiftCouponStatePort
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.UseGiftCouponCodeStatePort
import com.example.estdelivery.coupon.application.port.out.ValidateGiftCouponCodeStatePort
import com.example.estdelivery.coupon.application.utils.TransactionArea
import org.springframework.context.annotation.Bean
Expand All @@ -30,4 +32,19 @@ class ServiceBeanManager {
validateGiftCouponCodeStatePort,
transactionArea,
)

@Bean
fun enrollCouponByMessageUseCase(
loadMemberStatePort: LoadMemberStatePort,
useGiftCouponCodeStatePort: UseGiftCouponCodeStatePort,
loadGiftCouponStatePort: LoadGiftCouponStatePort,
updateMemberStatePort: UpdateMemberStatePort,
transactionArea: TransactionArea,
) = EnrollCouponByMessageService(
loadMemberStatePort,
useGiftCouponCodeStatePort,
loadGiftCouponStatePort,
updateMemberStatePort,
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.GiftCouponCode

interface EnrollCouponByMessageUseCase {
fun enroll(memberId: Long, code: GiftCouponCode)
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.example.estdelivery.coupon.application.port.`in`.web

import com.example.estdelivery.coupon.application.port.`in`.EnrollCouponByMessageUseCase
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 com.example.estdelivery.coupon.domain.coupon.GiftCouponCode
import java.net.URL
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
Expand All @@ -16,14 +18,17 @@ import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

private const val MEMBER_ID = "Member-ID"

@RestController
@RequestMapping("/gift-coupons")
class GiftCouponMessageController(
private val findAvailableGiftCouponUseCase: FindAvailableGiftCouponUseCase,
private val giftCouponByMessageUseCase: GiftCouponByMessageUseCase,
private val enrollCouponByMessageUseCase: EnrollCouponByMessageUseCase,
) {
@GetMapping
fun findAvailableGiftCoupons(@RequestHeader(value = "Member-ID") memberId: Long): GiftCouponResponses =
fun findAvailableGiftCoupons(@RequestHeader(value = MEMBER_ID) memberId: Long): GiftCouponResponses =
findAvailableGiftCouponUseCase.findAvailableGiftCoupon(memberId)
.map {
GiftCouponResponse(
Expand All @@ -39,7 +44,7 @@ class GiftCouponMessageController(

@PostMapping("/send/{couponId}")
fun sendGiftAvailableCoupon(
@RequestHeader(value = "Member-ID") memberId: Long,
@RequestHeader(value = MEMBER_ID) memberId: Long,
@RequestParam message: String,
@PathVariable couponId: Long,
): GiftMessageResponse =
Expand All @@ -54,4 +59,10 @@ class GiftCouponMessageController(
enrollHref = URL("http", "localhost", 8080, "/gift-coupons/enroll/${it.giftCouponCode.code}")
)
}

@GetMapping("/enroll/{code}")
fun enrollGiftCoupon(
@RequestHeader(value = MEMBER_ID) memberId: Long,
@PathVariable code: String
) = enrollCouponByMessageUseCase.enroll(memberId, GiftCouponCode(code))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.example.estdelivery.coupon.application.port.out

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

interface LoadGiftCouponStatePort {
fun findGiftCoupon(giftCouponCode: GiftCouponCode): GiftCoupon
}
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 UseGiftCouponCodeStatePort {
fun useBy(giftCouponCode: GiftCouponCode)
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ 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
@@ -1,21 +1,26 @@
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.LoadGiftCouponStatePort
import com.example.estdelivery.coupon.application.port.out.UseGiftCouponCodeStatePort
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.mapper.toCoupon
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 jakarta.transaction.Transactional
import org.springframework.stereotype.Component

@Component
class GiftCouponMessageAdapter(
private val giftMessageRepository: GiftMessageRepository,
) : CreateGiftCouponMessageStatePort, ValidateGiftCouponCodeStatePort {
) : CreateGiftCouponMessageStatePort, ValidateGiftCouponCodeStatePort, UseGiftCouponCodeStatePort,
LoadGiftCouponStatePort {
override fun create(sender: Member, coupon: Coupon, message: String, giftCouponCode: GiftCouponCode): GiftMessage {
val giftMessageEntity = giftMessageRepository.save(
GiftMessageEntity(
Expand All @@ -36,4 +41,17 @@ class GiftCouponMessageAdapter(
override fun validate(giftCouponCode: GiftCouponCode): Boolean {
return giftMessageRepository.existsByEnrollCode(giftCouponCode.code)
}

@Transactional
override fun useBy(giftCouponCode: GiftCouponCode) {
val messageEntity = (giftMessageRepository.findByEnrollCode(giftCouponCode.code)
?: throw IllegalArgumentException("GiftCouponCode not found"))
messageEntity.isUsed = true
}

override fun findGiftCoupon(giftCouponCode: GiftCouponCode): GiftCoupon {
return giftMessageRepository.findByEnrollCode(giftCouponCode.code)
?.let { GiftCoupon(toCoupon(it.coupon), it.isUsed) }
?: throw IllegalArgumentException("GiftCouponCode not found")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ import java.time.LocalDate
@Table(name = "gift_message")
class GiftMessageEntity(
val message: String,
@Column(name = "sender_id")
@Column(name = "sender_id", nullable = false)
val sender: Long,
@Column(unique = true, nullable = false)
val enrollCode: String,
@OneToOne
@JoinColumn(name = "coupon_id")
@JoinColumn(name = "coupon_id", nullable = false)
val coupon: CouponEntity,
@Column(nullable = false)
val enrollDate: LocalDate = LocalDate.now(),
val isUsed: Boolean = false,
var isUsed: Boolean = false,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository

interface GiftMessageRepository : JpaRepository<GiftMessageEntity, Long> {
fun existsByEnrollCode(enrollCode: String): Boolean
fun findByEnrollCode(enrollCode: String): GiftMessageEntity?
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ package com.example.estdelivery.coupon.domain.coupon
* 선물하는 쿠폰을 선정할 때 여러 제약이 생길 수 있다.
* 제약 변경에 대응하기 위해 컴포지션을 활용한다.
*/
class GiftCoupon(
val coupon: Coupon
data class GiftCoupon(
val coupon: Coupon,
val isUsed: Boolean = false,
) {
init {
require(!coupon.isPublished()) { "발행한 쿠폰은 선물할 수 없습니다." }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.example.estdelivery.coupon.application

import com.example.estdelivery.coupon.application.port.out.LoadGiftCouponStatePort
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.UseGiftCouponCodeStatePort
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.fixture.나눠준_비율_할인_쿠폰
import com.example.estdelivery.coupon.domain.fixture.일건창
import com.example.estdelivery.coupon.domain.member.Member
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.collections.shouldContain
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot

class EnrollCouponByMessageServiceTest : FreeSpec({
val loadMemberStatePort = mockk<LoadMemberStatePort>()
val useGiftCouponCodeStatePort = mockk<UseGiftCouponCodeStatePort>()
val loadGiftCouponStatePort = mockk<LoadGiftCouponStatePort>()
val updateMemberStatePort = mockk<UpdateMemberStatePort>()

lateinit var enrollCouponByMessageService: EnrollCouponByMessageService

beforeTest {
enrollCouponByMessageService = EnrollCouponByMessageService(
loadMemberStatePort,
useGiftCouponCodeStatePort,
loadGiftCouponStatePort,
updateMemberStatePort,
TransactionArea(),
)
}

"메시지로 받은 쿠폰을 등록한다." {
// given
val member = 일건창()
val giftCouponCode = GiftCouponCode.create()
val coupon = 나눠준_비율_할인_쿠폰
val giftCoupon = GiftCoupon(coupon)
val updatedMember = slot<Member>()

// when
every { loadMemberStatePort.findMember(member.id) } returns member
every { loadGiftCouponStatePort.findGiftCoupon(giftCouponCode) } returns giftCoupon
every { useGiftCouponCodeStatePort.useBy(giftCouponCode) } returns Unit
every { updateMemberStatePort.updateMembersCoupon(capture(updatedMember)) } returns Unit

enrollCouponByMessageService.enroll(member.id, giftCouponCode)

// then
updatedMember.captured.showMyCouponBook() shouldContain coupon
}

"메시지로 받은 쿠폰 코드는 사용된적이 없으면 예외가 발생한다." {
// given
val member = 일건창()
val giftCouponCode = GiftCouponCode.create()
val coupon = 나눠준_비율_할인_쿠폰
val giftCoupon = GiftCoupon(coupon, true)

// when
every { loadMemberStatePort.findMember(member.id) } returns member
every { loadGiftCouponStatePort.findGiftCoupon(giftCouponCode) } returns giftCoupon

// then
shouldThrow<IllegalArgumentException> {
enrollCouponByMessageService.enroll(member.id, giftCouponCode)
}
}
})
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.estdelivery.coupon.application.port.`in`.web

import com.example.estdelivery.coupon.application.port.`in`.EnrollCouponByMessageUseCase
import com.example.estdelivery.coupon.application.port.`in`.FindAvailableGiftCouponUseCase
import com.example.estdelivery.coupon.application.port.`in`.GiftCouponByMessageUseCase
import com.example.estdelivery.coupon.domain.coupon.GiftCoupon
Expand Down Expand Up @@ -28,6 +29,9 @@ class GiftCouponMessageControllerTest {
@MockkBean
lateinit var giftCouponByMessageUseCase: GiftCouponByMessageUseCase

@MockkBean
lateinit var enrollCouponByMessageUseCase: EnrollCouponByMessageUseCase

@Autowired
lateinit var mockMvc: MockMvc

Expand Down Expand Up @@ -85,4 +89,21 @@ class GiftCouponMessageControllerTest {
}
}
}

@Test
fun `쿠폰 코드를 입력해 쿠폰을 등록한다`() {
// given
val giftCouponCode = GiftCouponCode.create()
val 일건창 = 일건창()

// when
every { enrollCouponByMessageUseCase.enroll(일건창.id, giftCouponCode) } returns Unit

// then
mockMvc.get("/gift-coupons/enroll/{code}", giftCouponCode.code) {
header(MEMBER_ID, 일건창.id)
}.andExpect {
status { isOk() }
}
}
}
Loading
Loading