feat(admin-charge): 관리자 캔 환불 API로 미사용 7일 이내 환불을 처리한다
This commit is contained in:
38
docs/20260305_캔환불API생성.md
Normal file
38
docs/20260305_캔환불API생성.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
- [x] 기존 charge/payment/member 및 admin API 패턴 확인
|
||||||
|
- [x] `kr.co.vividnext.sodalive.admin.charge` 패키지에 캔 환불 API 생성
|
||||||
|
- [x] 환불 조건 검증 구현 (미사용, 7일 이내)
|
||||||
|
- [x] ChargeEntity/PaymentEntity/MemberEntity 환불 반영 로직 구현
|
||||||
|
- [x] 캔 환불 API 테스트 코드 작성
|
||||||
|
- [x] 검증 실행 및 결과 기록
|
||||||
|
|
||||||
|
## 환불 조건 상세
|
||||||
|
- 환불 가능 충전내역 조건: `charge.status == CHARGE` 그리고 `payment.status == COMPLETE`
|
||||||
|
- 이미 사용한 캔 판정 조건: `charge.title`에서 숫자를 추출해 현재 `chargeCan/rewardCan`과 비교
|
||||||
|
- 예시1) `100 캔 + 50 캔` -> `chargeCan = 100`, `rewardCan = 50`
|
||||||
|
- 예시2) `5,000 캔 + 500 캔` -> `chargeCan = 5000`, `rewardCan = 500`
|
||||||
|
- 예시3) `500캔` -> `chargeCan = 500`
|
||||||
|
- 예시4) `4,000 캔` -> `chargeCan = 4000`
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
|
||||||
|
### 1차 구현
|
||||||
|
- 무엇을: 관리자 캔 환불 API(`POST /admin/charge/refund`)와 환불 서비스/요청 DTO, i18n 메시지, 단위 테스트를 추가했다.
|
||||||
|
- 왜: 사용하지 않은 캔만 7일 이내 환불 가능하도록 하고, 환불 시 Charge/Payment/Member 상태를 요구사항대로 갱신하기 위해.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew test --tests "kr.co.vividnext.sodalive.admin.charge.AdminChargeRefundServiceTest"` 실행 → 성공
|
||||||
|
- `./gradlew build` 실행 → 성공 (ktlint/check/test/build 포함)
|
||||||
|
- LSP 진단 시도(`lsp_diagnostics`) → Kotlin LSP 미설정으로 불가, 대신 Gradle 컴파일/ktlint/test/build로 검증
|
||||||
|
|
||||||
|
### 2차 수정
|
||||||
|
- 무엇을: `AdminChargeRefundServiceTest`에 한글 `@DisplayName`을 추가하고, 각 테스트 문단에 given/when/then 역할 주석을 보강했다.
|
||||||
|
- 왜: 테스트 의도를 한눈에 파악하고, 문단별 책임을 명확히 하기 위해.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew test --tests "kr.co.vividnext.sodalive.admin.charge.AdminChargeRefundServiceTest"` 실행 → 성공
|
||||||
|
- `./gradlew ktlintTestSourceSetCheck` 실행 → 성공
|
||||||
|
|
||||||
|
### 3차 수정
|
||||||
|
- 무엇을: 이미 사용한 캔 판정을 `charge.title` 숫자 파싱 비교 방식으로 변경하고, 단일 숫자/콤마 포함 제목 테스트 케이스를 추가했다.
|
||||||
|
- 왜: 환불 조건을 충전 제목 기반 비교 규칙(단일/복수 숫자, 콤마 포함)으로 명확하게 적용하기 위해.
|
||||||
|
- 어떻게:
|
||||||
|
- `./gradlew test --tests "kr.co.vividnext.sodalive.admin.charge.AdminChargeRefundServiceTest"` 실행 → 성공
|
||||||
|
- `./gradlew build` 실행 → 성공
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package kr.co.vividnext.sodalive.admin.charge
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
|
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.RestController
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
@RequestMapping("/admin/charge")
|
||||||
|
class AdminChargeRefundController(private val service: AdminChargeRefundService) {
|
||||||
|
@PostMapping("/refund")
|
||||||
|
fun refund(@RequestBody request: AdminChargeRefundRequest) = ApiResponse.ok(service.refund(request))
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package kr.co.vividnext.sodalive.admin.charge
|
||||||
|
|
||||||
|
data class AdminChargeRefundRequest(
|
||||||
|
val chargeId: Long
|
||||||
|
)
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package kr.co.vividnext.sodalive.admin.charge
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.can.charge.Charge
|
||||||
|
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
|
||||||
|
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
|
||||||
|
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 org.springframework.data.repository.findByIdOrNull
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.temporal.ChronoUnit
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AdminChargeRefundService(
|
||||||
|
private val chargeRepository: ChargeRepository,
|
||||||
|
private val messageSource: SodaMessageSource,
|
||||||
|
private val langContext: LangContext
|
||||||
|
) {
|
||||||
|
@Transactional
|
||||||
|
fun refund(request: AdminChargeRefundRequest) {
|
||||||
|
val charge = chargeRepository.findByIdOrNull(request.chargeId)
|
||||||
|
?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
val payment = charge.payment
|
||||||
|
?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
val member = charge.member
|
||||||
|
?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
|
||||||
|
if (charge.status != ChargeStatus.CHARGE || payment.status != PaymentStatus.COMPLETE) {
|
||||||
|
throw SodaException(messageKey = "can.payment.refund.invalid_request")
|
||||||
|
}
|
||||||
|
|
||||||
|
validateRefundDate(charge)
|
||||||
|
validateUnusedCan(charge)
|
||||||
|
|
||||||
|
deductMemberCan(member, payment.paymentGateway, charge.chargeCan, charge.rewardCan)
|
||||||
|
|
||||||
|
charge.chargeCan = 0
|
||||||
|
charge.rewardCan = 0
|
||||||
|
charge.status = ChargeStatus.CANCEL
|
||||||
|
payment.status = PaymentStatus.RETURN
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateUnusedCan(charge: Charge) {
|
||||||
|
val title = charge.title ?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
val (originalChargeCan, originalRewardCan) = extractCanFromTitle(title)
|
||||||
|
if (charge.chargeCan != originalChargeCan || charge.rewardCan != originalRewardCan) {
|
||||||
|
throw SodaException(messageKey = "can.payment.refund.used_not_allowed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractCanFromTitle(title: String): Pair<Int, Int> {
|
||||||
|
val parsedNumbers = TITLE_CAN_REGEX
|
||||||
|
.findAll(title)
|
||||||
|
.map { it.value.replace(",", "").toIntOrNull() }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
if (parsedNumbers.isEmpty() || parsedNumbers.first() == null) {
|
||||||
|
throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
}
|
||||||
|
|
||||||
|
val chargeCanFromTitle = parsedNumbers.first()!!
|
||||||
|
val rewardCanFromTitle = parsedNumbers.getOrNull(1) ?: 0
|
||||||
|
return Pair(chargeCanFromTitle, rewardCanFromTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateRefundDate(charge: Charge) {
|
||||||
|
val chargedAt = charge.createdAt
|
||||||
|
?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||||
|
val now = LocalDateTime.now()
|
||||||
|
|
||||||
|
if (now.isAfter(chargedAt.plusDays(7))) {
|
||||||
|
val passedDays = ChronoUnit.DAYS.between(chargedAt.toLocalDate(), now.toLocalDate())
|
||||||
|
val messageTemplate = messageSource.getMessage("can.payment.refund.days_exceeded", langContext.lang)
|
||||||
|
?: "충천 후 %s일이 지나서 환불할 수 없습니다."
|
||||||
|
throw SodaException(message = String.format(messageTemplate, passedDays))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deductMemberCan(member: Member, paymentGateway: PaymentGateway, chargeCan: Int, rewardCan: Int) {
|
||||||
|
when (paymentGateway) {
|
||||||
|
PaymentGateway.GOOGLE_IAP -> {
|
||||||
|
member.googleChargeCan -= chargeCan
|
||||||
|
member.googleRewardCan -= rewardCan
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentGateway.APPLE_IAP -> {
|
||||||
|
member.appleChargeCan -= chargeCan
|
||||||
|
member.appleRewardCan -= rewardCan
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
member.pgChargeCan -= chargeCan
|
||||||
|
member.pgRewardCan -= rewardCan
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TITLE_CAN_REGEX = Regex("\\d[\\d,]*")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -614,6 +614,16 @@ class SodaMessageSource {
|
|||||||
Lang.KO to "%s 캔이 부족합니다. 충전 후 이용해 주세요.",
|
Lang.KO to "%s 캔이 부족합니다. 충전 후 이용해 주세요.",
|
||||||
Lang.EN to "You are short of %s cans. Please recharge and try again.",
|
Lang.EN to "You are short of %s cans. Please recharge and try again.",
|
||||||
Lang.JA to "%sCANが不足しています。チャージしてからご利用ください。"
|
Lang.JA to "%sCANが不足しています。チャージしてからご利用ください。"
|
||||||
|
),
|
||||||
|
"can.payment.refund.used_not_allowed" to mapOf(
|
||||||
|
Lang.KO to "사용한 캔은 환불할 수 없습니다.",
|
||||||
|
Lang.EN to "Used cans cannot be refunded.",
|
||||||
|
Lang.JA to "使用したCANは返金できません。"
|
||||||
|
),
|
||||||
|
"can.payment.refund.days_exceeded" to mapOf(
|
||||||
|
Lang.KO to "충천 후 %s일이 지나서 환불할 수 없습니다.",
|
||||||
|
Lang.EN to "Refund is not available because %s days have passed since charging.",
|
||||||
|
Lang.JA to "チャージ後%s日が経過しているため返金できません。"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
package kr.co.vividnext.sodalive.admin.charge
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.can.charge.Charge
|
||||||
|
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
|
||||||
|
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.Payment
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
|
||||||
|
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.i18n.Lang
|
||||||
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertThrows
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.DisplayName
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.util.Optional
|
||||||
|
|
||||||
|
@DisplayName("관리자 캔 환불 서비스 테스트")
|
||||||
|
class AdminChargeRefundServiceTest {
|
||||||
|
private lateinit var chargeRepository: ChargeRepository
|
||||||
|
private lateinit var messageSource: SodaMessageSource
|
||||||
|
private lateinit var langContext: LangContext
|
||||||
|
private lateinit var service: AdminChargeRefundService
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
// given: 테스트 대상 의존성을 목 객체로 준비한다.
|
||||||
|
chargeRepository = Mockito.mock(ChargeRepository::class.java)
|
||||||
|
messageSource = Mockito.mock(SodaMessageSource::class.java)
|
||||||
|
langContext = Mockito.mock(LangContext::class.java)
|
||||||
|
|
||||||
|
// given: 메시지 포맷 테스트를 위해 기본 언어를 한국어로 고정한다.
|
||||||
|
Mockito.`when`(langContext.lang).thenReturn(Lang.KO)
|
||||||
|
|
||||||
|
// given: 환불 서비스 인스턴스를 생성한다.
|
||||||
|
service = AdminChargeRefundService(
|
||||||
|
chargeRepository = chargeRepository,
|
||||||
|
messageSource = messageSource,
|
||||||
|
langContext = langContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("사용하지 않은 구글 인앱 캔을 7일 이내에 환불한다")
|
||||||
|
fun shouldRefundGoogleIapCanWhenUnusedWithinSevenDays() {
|
||||||
|
// given: 구글 인앱 충전 캔을 보유한 회원을 준비한다.
|
||||||
|
val member = Member(password = "password", nickname = "tester")
|
||||||
|
member.googleChargeCan = 50
|
||||||
|
member.googleRewardCan = 10
|
||||||
|
|
||||||
|
// given: 7일 이내 생성된 충전/결제 데이터를 준비한다.
|
||||||
|
val charge = Charge(50, 10, status = ChargeStatus.CHARGE)
|
||||||
|
charge.id = 1L
|
||||||
|
charge.title = "50 캔 + 10 캔"
|
||||||
|
charge.createdAt = LocalDateTime.now().minusDays(3)
|
||||||
|
charge.member = member
|
||||||
|
|
||||||
|
val payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.GOOGLE_IAP)
|
||||||
|
charge.payment = payment
|
||||||
|
|
||||||
|
// given: 조회 시 환불 대상 충전을 반환하도록 설정한다.
|
||||||
|
Mockito.`when`(chargeRepository.findById(1L)).thenReturn(Optional.of(charge))
|
||||||
|
|
||||||
|
// when: 관리자 환불을 실행한다.
|
||||||
|
service.refund(AdminChargeRefundRequest(chargeId = 1L))
|
||||||
|
|
||||||
|
// then: 충전/결제/회원 캔 상태가 환불 기준으로 변경된다.
|
||||||
|
assertEquals(0, charge.chargeCan)
|
||||||
|
assertEquals(0, charge.rewardCan)
|
||||||
|
assertEquals(ChargeStatus.CANCEL, charge.status)
|
||||||
|
assertEquals(PaymentStatus.RETURN, payment.status)
|
||||||
|
assertEquals(0, member.googleChargeCan)
|
||||||
|
assertEquals(0, member.googleRewardCan)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("사용하지 않은 PG 캔을 7일 이내에 환불한다 (콤마 포함 제목 파싱)")
|
||||||
|
fun shouldRefundPgCanWhenUnusedWithinSevenDays() {
|
||||||
|
// given: PG 충전 캔을 보유한 회원을 준비한다.
|
||||||
|
val member = Member(password = "password", nickname = "tester")
|
||||||
|
member.pgChargeCan = 5000
|
||||||
|
member.pgRewardCan = 500
|
||||||
|
|
||||||
|
// given: 콤마가 포함된 제목과 7일 이내 생성된 충전/결제 데이터를 준비한다.
|
||||||
|
val charge = Charge(5000, 500, status = ChargeStatus.CHARGE)
|
||||||
|
charge.id = 2L
|
||||||
|
charge.title = "5,000 캔 + 500 캔"
|
||||||
|
charge.createdAt = LocalDateTime.now().minusDays(1)
|
||||||
|
charge.member = member
|
||||||
|
|
||||||
|
val payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG)
|
||||||
|
charge.payment = payment
|
||||||
|
|
||||||
|
// given: 조회 시 환불 대상 충전을 반환하도록 설정한다.
|
||||||
|
Mockito.`when`(chargeRepository.findById(2L)).thenReturn(Optional.of(charge))
|
||||||
|
|
||||||
|
// when: 관리자 환불을 실행한다.
|
||||||
|
service.refund(AdminChargeRefundRequest(chargeId = 2L))
|
||||||
|
|
||||||
|
// then: 충전/결제/회원 캔 상태가 환불 기준으로 변경된다.
|
||||||
|
assertEquals(0, charge.chargeCan)
|
||||||
|
assertEquals(0, charge.rewardCan)
|
||||||
|
assertEquals(ChargeStatus.CANCEL, charge.status)
|
||||||
|
assertEquals(PaymentStatus.RETURN, payment.status)
|
||||||
|
assertEquals(0, member.pgChargeCan)
|
||||||
|
assertEquals(0, member.pgRewardCan)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("제목이 단일 숫자(500캔)인 경우 chargeCan만 비교해 환불한다")
|
||||||
|
fun shouldRefundWhenTitleContainsOnlyChargeCanWithoutSpace() {
|
||||||
|
// given: 단일 숫자 제목과 일치하는 PG 충전 데이터를 준비한다.
|
||||||
|
val member = Member(password = "password", nickname = "tester")
|
||||||
|
member.pgChargeCan = 500
|
||||||
|
member.pgRewardCan = 0
|
||||||
|
|
||||||
|
val charge = Charge(500, 0, status = ChargeStatus.CHARGE)
|
||||||
|
charge.id = 21L
|
||||||
|
charge.title = "500캔"
|
||||||
|
charge.createdAt = LocalDateTime.now().minusDays(2)
|
||||||
|
charge.member = member
|
||||||
|
|
||||||
|
val payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG)
|
||||||
|
charge.payment = payment
|
||||||
|
|
||||||
|
// given: 조회 시 환불 대상 충전을 반환하도록 설정한다.
|
||||||
|
Mockito.`when`(chargeRepository.findById(21L)).thenReturn(Optional.of(charge))
|
||||||
|
|
||||||
|
// when: 관리자 환불을 실행한다.
|
||||||
|
service.refund(AdminChargeRefundRequest(chargeId = 21L))
|
||||||
|
|
||||||
|
// then: 단일 숫자 제목을 chargeCan으로 파싱해 정상 환불된다.
|
||||||
|
assertEquals(0, charge.chargeCan)
|
||||||
|
assertEquals(0, charge.rewardCan)
|
||||||
|
assertEquals(ChargeStatus.CANCEL, charge.status)
|
||||||
|
assertEquals(PaymentStatus.RETURN, payment.status)
|
||||||
|
assertEquals(0, member.pgChargeCan)
|
||||||
|
assertEquals(0, member.pgRewardCan)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("제목이 단일 숫자(4,000 캔)인 경우 콤마를 제거하고 chargeCan을 비교한다")
|
||||||
|
fun shouldRefundWhenTitleContainsOnlyChargeCanWithComma() {
|
||||||
|
// given: 콤마가 포함된 단일 숫자 제목과 일치하는 PG 충전 데이터를 준비한다.
|
||||||
|
val member = Member(password = "password", nickname = "tester")
|
||||||
|
member.pgChargeCan = 4000
|
||||||
|
member.pgRewardCan = 0
|
||||||
|
|
||||||
|
val charge = Charge(4000, 0, status = ChargeStatus.CHARGE)
|
||||||
|
charge.id = 22L
|
||||||
|
charge.title = "4,000 캔"
|
||||||
|
charge.createdAt = LocalDateTime.now().minusDays(2)
|
||||||
|
charge.member = member
|
||||||
|
|
||||||
|
val payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG)
|
||||||
|
charge.payment = payment
|
||||||
|
|
||||||
|
// given: 조회 시 환불 대상 충전을 반환하도록 설정한다.
|
||||||
|
Mockito.`when`(chargeRepository.findById(22L)).thenReturn(Optional.of(charge))
|
||||||
|
|
||||||
|
// when: 관리자 환불을 실행한다.
|
||||||
|
service.refund(AdminChargeRefundRequest(chargeId = 22L))
|
||||||
|
|
||||||
|
// then: 콤마를 제거한 값(4000)으로 비교해 정상 환불된다.
|
||||||
|
assertEquals(0, charge.chargeCan)
|
||||||
|
assertEquals(0, charge.rewardCan)
|
||||||
|
assertEquals(ChargeStatus.CANCEL, charge.status)
|
||||||
|
assertEquals(PaymentStatus.RETURN, payment.status)
|
||||||
|
assertEquals(0, member.pgChargeCan)
|
||||||
|
assertEquals(0, member.pgRewardCan)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("이미 사용한 캔은 환불할 수 없다")
|
||||||
|
fun shouldThrowWhenCanWasUsed() {
|
||||||
|
// given: 제목 숫자(원본) 대비 현재 charge 수량이 감소한 데이터를 준비한다.
|
||||||
|
val member = Member(password = "password", nickname = "tester")
|
||||||
|
member.pgChargeCan = 80
|
||||||
|
member.pgRewardCan = 50
|
||||||
|
|
||||||
|
val charge = Charge(80, 50, status = ChargeStatus.CHARGE)
|
||||||
|
charge.id = 3L
|
||||||
|
charge.title = "100 캔 + 50 캔"
|
||||||
|
charge.createdAt = LocalDateTime.now().minusDays(2)
|
||||||
|
charge.member = member
|
||||||
|
|
||||||
|
val payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG)
|
||||||
|
charge.payment = payment
|
||||||
|
|
||||||
|
// given: 조회 시 사용 이력이 있는 충전을 반환하도록 설정한다.
|
||||||
|
Mockito.`when`(chargeRepository.findById(3L)).thenReturn(Optional.of(charge))
|
||||||
|
|
||||||
|
// when: 관리자 환불을 실행하고 예외를 수집한다.
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.refund(AdminChargeRefundRequest(chargeId = 3L))
|
||||||
|
}
|
||||||
|
|
||||||
|
// then: 사용 캔 환불 불가 메시지 키가 반환된다.
|
||||||
|
assertEquals("can.payment.refund.used_not_allowed", exception.messageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("충전 후 7일이 지난 캔은 환불할 수 없다")
|
||||||
|
fun shouldThrowWhenChargedMoreThanSevenDaysAgo() {
|
||||||
|
// given: 7일을 초과한 충전 데이터를 준비한다.
|
||||||
|
val member = Member(password = "password", nickname = "tester")
|
||||||
|
member.pgChargeCan = 100
|
||||||
|
member.pgRewardCan = 20
|
||||||
|
|
||||||
|
val charge = Charge(100, 20, status = ChargeStatus.CHARGE)
|
||||||
|
charge.id = 4L
|
||||||
|
charge.title = "100 캔 + 20 캔"
|
||||||
|
charge.createdAt = LocalDateTime.now().minusDays(10)
|
||||||
|
charge.member = member
|
||||||
|
|
||||||
|
val payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG)
|
||||||
|
charge.payment = payment
|
||||||
|
|
||||||
|
// given: 조회 및 메시지 템플릿 반환 동작을 설정한다.
|
||||||
|
Mockito.`when`(chargeRepository.findById(4L)).thenReturn(Optional.of(charge))
|
||||||
|
Mockito.`when`(
|
||||||
|
messageSource.getMessage("can.payment.refund.days_exceeded", Lang.KO)
|
||||||
|
).thenReturn("충천 후 %s일이 지나서 환불할 수 없습니다.")
|
||||||
|
|
||||||
|
// when: 관리자 환불을 실행하고 예외를 수집한다.
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.refund(AdminChargeRefundRequest(chargeId = 4L))
|
||||||
|
}
|
||||||
|
|
||||||
|
// then: 일수 포함 환불 불가 메시지가 반환된다.
|
||||||
|
assertTrue(exception.message!!.startsWith("충천 후 "))
|
||||||
|
assertTrue(exception.message!!.contains("환불할 수 없습니다."))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("존재하지 않는 충전 건은 환불할 수 없다")
|
||||||
|
fun shouldThrowWhenChargeNotFound() {
|
||||||
|
// given: 환불 대상 충전이 존재하지 않도록 설정한다.
|
||||||
|
Mockito.`when`(chargeRepository.findById(999L)).thenReturn(Optional.empty())
|
||||||
|
|
||||||
|
// when: 관리자 환불을 실행하고 예외를 수집한다.
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.refund(AdminChargeRefundRequest(chargeId = 999L))
|
||||||
|
}
|
||||||
|
|
||||||
|
// then: 잘못된 요청 메시지 키가 반환된다.
|
||||||
|
assertEquals("common.error.invalid_request", exception.messageKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user