diff --git a/docs/20260402_쿠폰사용본인인증예외추가.md b/docs/20260402_쿠폰사용본인인증예외추가.md new file mode 100644 index 00000000..a3c81038 --- /dev/null +++ b/docs/20260402_쿠폰사용본인인증예외추가.md @@ -0,0 +1,10 @@ +- [x] `CanCouponService.useCanCoupon`의 기존 본인인증 요구 조건과 국가/성인노출 관련 패턴을 확인한다. +- [x] 한국이 아닌 국가에서 `MemberContentPreference.isAdultContentVisibl`가 `true`이면 본인인증 없이 쿠폰 사용이 가능하도록 수정한다. +- [x] 변경 파일 진단과 관련 검증을 수행하고 결과를 기록한다. + +## 검증 기록 + +### 1차 구현 +- 무엇을: `CanCouponService.useCanCoupon`이 `MemberContentPreferenceService.getStoredPreference(member).isAdult`를 기준으로 쿠폰 사용 가능 여부를 판단하도록 수정하고, 해당 분기 회귀 테스트를 추가했다. +- 왜: 한국 사용자는 기존처럼 본인인증이 필요하고, 한국이 아닌 사용자는 성인 노출 설정이 `true`이면 본인인증 없이 쿠폰을 사용할 수 있어야 하기 때문이다. +- 어떻게: `./gradlew test --tests "kr.co.vividnext.sodalive.can.coupon.CanCouponServiceTest"` 실행 성공, `./gradlew ktlintCheck` 실행 성공. diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt index 54f17365..b457778d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponService.kt @@ -8,6 +8,7 @@ import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService import org.apache.poi.xssf.usermodel.XSSFWorkbook import org.springframework.context.ApplicationEventPublisher import org.springframework.data.repository.findByIdOrNull @@ -29,6 +30,7 @@ class CanCouponService( private val couponNumberRepository: CanCouponNumberRepository, private val memberRepository: MemberRepository, + private val memberContentPreferenceService: MemberContentPreferenceService, private val objectMapper: ObjectMapper, private val applicationEventPublisher: ApplicationEventPublisher, @@ -133,7 +135,8 @@ class CanCouponService( val member = memberRepository.findByIdOrNull(id = memberId) ?: throw SodaException(messageKey = "common.error.bad_credentials") - if (member.auth == null) throw SodaException(messageKey = "can.coupon.auth_required") + val viewerContentPreference = memberContentPreferenceService.getStoredPreference(member) + if (!viewerContentPreference.isAdult) throw SodaException(messageKey = "can.coupon.auth_required") issueService.validateAvailableUseCoupon(couponNumber, memberId) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponServiceTest.kt new file mode 100644 index 00000000..651ec3fb --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/can/coupon/CanCouponServiceTest.kt @@ -0,0 +1,122 @@ +package kr.co.vividnext.sodalive.can.coupon + +import com.fasterxml.jackson.databind.ObjectMapper +import kr.co.vividnext.sodalive.can.charge.ChargeService +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.i18n.LangContext +import kr.co.vividnext.sodalive.i18n.SodaMessageSource +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService +import kr.co.vividnext.sodalive.member.contentpreference.ViewerContentPreference +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.springframework.context.ApplicationEventPublisher +import java.util.Optional + +class CanCouponServiceTest { + private lateinit var issueService: CanCouponIssueService + private lateinit var chargeService: ChargeService + private lateinit var repository: CanCouponRepository + private lateinit var couponNumberRepository: CanCouponNumberRepository + private lateinit var memberRepository: MemberRepository + private lateinit var memberContentPreferenceService: MemberContentPreferenceService + private lateinit var objectMapper: ObjectMapper + private lateinit var applicationEventPublisher: ApplicationEventPublisher + private lateinit var messageSource: SodaMessageSource + private lateinit var langContext: LangContext + + private lateinit var service: CanCouponService + + @BeforeEach + fun setUp() { + issueService = mock(CanCouponIssueService::class.java) + chargeService = mock(ChargeService::class.java) + repository = mock(CanCouponRepository::class.java) + couponNumberRepository = mock(CanCouponNumberRepository::class.java) + memberRepository = mock(MemberRepository::class.java) + memberContentPreferenceService = mock(MemberContentPreferenceService::class.java) + objectMapper = mock(ObjectMapper::class.java) + applicationEventPublisher = mock(ApplicationEventPublisher::class.java) + messageSource = mock(SodaMessageSource::class.java) + langContext = LangContext() + + service = CanCouponService( + issueService = issueService, + chargeService = chargeService, + repository = repository, + couponNumberRepository = couponNumberRepository, + memberRepository = memberRepository, + memberContentPreferenceService = memberContentPreferenceService, + objectMapper = objectMapper, + applicationEventPublisher = applicationEventPublisher, + messageSource = messageSource, + langContext = langContext + ) + } + + @Test + @DisplayName("비한국 사용자는 성인 노출 설정이 true이면 본인인증 없이 쿠폰을 사용할 수 있다") + fun shouldUseCouponWithoutAuthForNonKrAdultVisibleMember() { + val member = createMember(memberId = 1L) + val couponNumber = "COUPON1234" + + `when`(memberRepository.findById(1L)).thenReturn(Optional.of(member)) + `when`(memberContentPreferenceService.getStoredPreference(member)).thenReturn( + ViewerContentPreference( + countryCode = "US", + isAdultContentVisible = true, + contentType = kr.co.vividnext.sodalive.content.ContentType.ALL, + isAdult = true + ) + ) + `when`(chargeService.chargeByCoupon(couponNumber, member)).thenReturn("charged") + + val result = service.useCanCoupon(couponNumber = couponNumber, memberId = 1L) + + assertEquals("charged", result) + verify(issueService).validateAvailableUseCoupon(couponNumber, 1L) + verify(chargeService).chargeByCoupon(couponNumber, member) + } + + @Test + @DisplayName("한국 사용자는 본인인증이 없으면 기존처럼 쿠폰 사용이 불가능하다") + fun shouldThrowWhenAdultPolicyIsFalse() { + val member = createMember(memberId = 2L) + + `when`(memberRepository.findById(2L)).thenReturn(Optional.of(member)) + `when`(memberContentPreferenceService.getStoredPreference(member)).thenReturn( + ViewerContentPreference( + countryCode = "KR", + isAdultContentVisible = true, + contentType = kr.co.vividnext.sodalive.content.ContentType.ALL, + isAdult = false + ) + ) + + val exception = assertThrows(SodaException::class.java) { + service.useCanCoupon(couponNumber = "COUPON5678", memberId = 2L) + } + + assertEquals("can.coupon.auth_required", exception.messageKey) + verify(issueService, never()).validateAvailableUseCoupon("COUPON5678", 2L) + verify(chargeService, never()).chargeByCoupon("COUPON5678", member) + } + + private fun createMember(memberId: Long): Member { + return Member( + email = "member$memberId@test.com", + password = "password", + nickname = "member$memberId" + ).apply { + id = memberId + } + } +}