Skip to content
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
Expand Up @@ -80,7 +80,7 @@ public AlbumCreateResponse createAlbum(AlbumCreateRequest request) {

validatePaymentMemberMismatch(payment, currentMember);

payment.updatePayment(PaymentPurpose.CREATION, album);
payment.assignToAlbum(PaymentPurpose.CREATION, album);

subscriptionRepository.save(
Subscription.createSubscription(currentMember, album, payment.getPaidAt()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.cherrypic.global.annotation.PageSize;
import org.cherrypic.global.pagination.SliceResponse;
import org.cherrypic.global.pagination.SortDirection;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

Expand Down Expand Up @@ -42,6 +43,13 @@ public PaymentVerificationResponse paymentVerify(@PathVariable String impUid) {
return paymentService.verifyPayment(impUid);
}

@PostMapping("/cancel/{impUid}")
@Operation(summary = "impUid 기반 결제 취소", description = "impUid에 해당하는 결제를 전체 환불 처리합니다.")
public ResponseEntity<Void> paymentCancel(@PathVariable String impUid) {
paymentService.cancelPayment(impUid);
return ResponseEntity.noContent().build();
}

@GetMapping("/unlinked")
@Operation(
summary = "앨범과 연결되지 않은 완료된 결제 내역 조회",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@

public interface PaymentRepository extends JpaRepository<Payment, Long>, PaymentRepositoryCustom {
Optional<Payment> findByMerchantUid(String merchantUid);

Optional<Payment> findByImpUid(String impUid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public interface PaymentService {

PaymentVerificationResponse verifyPayment(String impUid);

void cancelPayment(String impUid);

PaymentUnlinkedResponse getUnlinkedPayment();

SliceResponse<PaymentListResponse> getAlbumPayments(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.siot.IamportRestClient.IamportClient;
import com.siot.IamportRestClient.exception.IamportResponseException;
import com.siot.IamportRestClient.request.CancelData;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
Expand Down Expand Up @@ -108,7 +109,7 @@ public PaymentVerificationResponse verifyPayment(String impUid) {
throw new CustomException(PaymentErrorCode.NOT_PAID);
}

payment.updatePayment(impUid, pgProvider, PaymentStatus.PAID, paidAt);
payment.complete(impUid, pgProvider, paidAt);

return PaymentVerificationResponse.from(payment);

Expand All @@ -119,6 +120,31 @@ public PaymentVerificationResponse verifyPayment(String impUid) {
}
}

@Override
public void cancelPayment(String impUid) {
try {
var iamportPayment = iamportClient.paymentByImpUid(impUid).getResponse();

Payment payment =
paymentRepository
.findByImpUid(impUid)
.orElseThrow(
() -> new CustomException(PaymentErrorCode.PAYMENT_NOT_FOUND));

payment.cancel(LocalDateTime.now());

CancelData cancelData =
new CancelData(iamportPayment.getImpUid(), true, iamportPayment.getAmount());

iamportClient.cancelPaymentByImpUid(cancelData);

} catch (IamportResponseException e) {
throw new CustomException(PaymentErrorCode.PAYMENT_NOT_FOUND);
} catch (IOException e) {
throw new CustomException(PaymentErrorCode.IAMPORT_API_UNAVAILABLE);
}
}

@Override
@Transactional(readOnly = true)
public SliceResponse<PaymentListResponse> getAlbumPayments(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public SubscriptionRenewResponse renewSubscription(

validatePaymentMemberMismatch(payment, currentMember);

payment.updatePayment(PaymentPurpose.RENEWAL, album);
payment.assignToAlbum(PaymentPurpose.RENEWAL, album);

final Subscription subscription = getSubscriptionByAlbumId(album.getId());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,31 @@ class PRO_또는_PREMIUM_유형인_경우 {
}

@Test
void 결제상태가_PAID가_아니면_예외가_발생한다() throws Exception {
void 이미_취소된_결제라면_예외가_발생한다() throws Exception {
// given
AlbumCreateRequest request =
new AlbumCreateRequest(
"testTitle", "testCoverUrl", AlbumType.PRO, 1L, false);

given(albumService.createAlbum(request))
.willThrow(new CustomException(PaymentDomainErrorCode.ALREADY_CANCELED));

// when & then
ResultActions perform =
mockMvc.perform(
post("/albums")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)));

perform.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()))
.andExpect(jsonPath("$.data.code").value("ALREADY_CANCELED"))
.andExpect(jsonPath("$.data.message").value("해당 결제는 이미 취소되었습니다."));
}

@Test
void 완료되지_않은_결제라면_예외가_발생한다() throws Exception {
// given
AlbumCreateRequest request =
new AlbumCreateRequest(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,8 @@ void setUp() {
5900,
PaymentPurpose.CREATION,
AlbumType.PRO);
payment1.updatePayment(
"testImpUid",
"testPgProvider",
PaymentStatus.PAID,
LocalDateTime.of(2025, 8, 1, 13, 0));
payment1.complete(
"testImpUid", "testPgProvider", LocalDateTime.of(2025, 8, 1, 13, 0));
// 검증되지 않은 결제
Payment payment2 =
Payment.createPayment(
Expand All @@ -223,9 +220,8 @@ void setUp() {
5900,
PaymentPurpose.CREATION,
AlbumType.PRO);
payment3.updatePayment(
"testImpUid", "testPgProvider", PaymentStatus.PAID, LocalDateTime.now());
payment3.updatePayment(PaymentPurpose.CREATION, album);
payment3.complete("testImpUid", "testPgProvider", LocalDateTime.now());
payment3.assignToAlbum(PaymentPurpose.CREATION, album);
// 구독 갱신 목적으로 쓰인 결제
Payment payment4 =
Payment.createPayment(
Expand All @@ -234,9 +230,19 @@ void setUp() {
5900,
PaymentPurpose.RENEWAL,
AlbumType.PRO);
payment4.updatePayment(
"testImpUid", "testPgProvider", PaymentStatus.PAID, LocalDateTime.now());
paymentRepository.saveAll(List.of(payment1, payment2, payment3, payment4));
payment4.complete("testImpUid", "testPgProvider", LocalDateTime.now());
// 취소된 결제
Payment payment5 =
Payment.createPayment(
member1,
"testMerchantUid",
5900,
PaymentPurpose.CREATION,
AlbumType.PRO);
payment5.complete("testImpUid", "testPgProvider", LocalDateTime.now());
payment5.cancel(LocalDateTime.now());
paymentRepository.saveAll(
List.of(payment1, payment2, payment3, payment4, payment5));
}

@Test
Expand Down Expand Up @@ -349,7 +355,20 @@ void setUp() {
}

@Test
void 결제상태가_PAID가_아니면_예외가_발생한다() {
void 이미_취소된_결제라면_예외가_발생한다() {
// given
AlbumCreateRequest request =
new AlbumCreateRequest(
"testTitle", "testCoverUrl", AlbumType.PRO, 5L, false);

// when & then
assertThatThrownBy(() -> albumService.createAlbum(request))
.isInstanceOf(CustomException.class)
.hasMessage(PaymentDomainErrorCode.ALREADY_CANCELED.getMessage());
}

@Test
void 완료되지_않은_결제라면_예외가_발생한다() {
// given
AlbumCreateRequest request =
new AlbumCreateRequest(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.cherrypic.payment.controller;

import static org.hamcrest.Matchers.nullValue;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
Expand All @@ -24,6 +24,7 @@
import org.cherrypic.global.pagination.SliceResponse;
import org.cherrypic.global.pagination.SortDirection;
import org.cherrypic.payment.enums.PaymentPurpose;
import org.cherrypic.payment.exception.PaymentDomainErrorCode;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
Expand Down Expand Up @@ -563,6 +564,110 @@ class 결제_검증_요청_시 {
}
}

@Nested
class 결제_취소_요청_시 {

@Test
void 유효한_요청이면_결제를_취소하고_NO_CONTENT_로_반환한다() throws Exception {
// given
willDoNothing().given(paymentService).cancelPayment("imp_1234");

// when & then
ResultActions perform = mockMvc.perform(post("/payments/cancel/imp_9999"));

perform.andExpect(status().isNoContent())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(HttpStatus.NO_CONTENT.value()));
}

@Test
void impUid가_아임포트에_존재하지_않으면_예외가_발생한다() throws Exception {
// given
willThrow(new CustomException(PaymentErrorCode.PAYMENT_NOT_FOUND))
.given(paymentService)
.cancelPayment("imp_9999");

// when & then
ResultActions perform = mockMvc.perform(post("/payments/cancel/imp_9999"));

perform.andExpect(status().isNotFound())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.NOT_FOUND.value()))
.andExpect(jsonPath("$.data.code").value("PAYMENT_NOT_FOUND"))
.andExpect(jsonPath("$.data.message").value("결제 정보가 존재하지 않습니다."));
}

@Test
void impUid에_해당하는_결제가_DB에_없으면_예외가_발생한다() throws Exception {
// given
willThrow(new CustomException(PaymentErrorCode.PAYMENT_NOT_FOUND))
.given(paymentService)
.cancelPayment("imp_9999");

// when & then
ResultActions perform = mockMvc.perform(post("/payments/cancel/imp_9999"));

perform.andExpect(status().isNotFound())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.NOT_FOUND.value()))
.andExpect(jsonPath("$.data.code").value("PAYMENT_NOT_FOUND"))
.andExpect(jsonPath("$.data.message").value("결제 정보가 존재하지 않습니다."));
}

@Test
void 이미_취소된_결제라면_예외가_발생한다() throws Exception {
// given
willThrow(new CustomException(PaymentDomainErrorCode.ALREADY_CANCELED))
.given(paymentService)
.cancelPayment("imp_9999");

// when & then
ResultActions perform = mockMvc.perform(post("/payments/cancel/imp_9999"));

perform.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()))
.andExpect(jsonPath("$.data.code").value("ALREADY_CANCELED"))
.andExpect(jsonPath("$.data.message").value("해당 결제는 이미 취소되었습니다."));
}

@Test
void 완료되지_않은_결제라면_예외가_발생한다() throws Exception {
// given
willThrow(new CustomException(PaymentDomainErrorCode.ONLY_PAID_PAYMENT_CANCELABLE))
.given(paymentService)
.cancelPayment("imp_9999");

// when & then
ResultActions perform = mockMvc.perform(post("/payments/cancel/imp_9999"));

perform.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()))
.andExpect(jsonPath("$.data.code").value("ONLY_PAID_PAYMENT_CANCELABLE"))
.andExpect(jsonPath("$.data.message").value("결제 취소는 완료된 결제만 가능합니다."));
}

@Test
void Iamport_API_통신_장애가_발생하면_예외가_발생한다() throws Exception {
// given
willThrow(new CustomException(PaymentErrorCode.IAMPORT_API_UNAVAILABLE))
.given(paymentService)
.cancelPayment("imp_9999");

// when & then
ResultActions perform = mockMvc.perform(post("/payments/cancel/imp_9999"));

perform.andExpect(status().isServiceUnavailable())
.andExpect(jsonPath("$.success").value(false))
.andExpect(jsonPath("$.status").value(HttpStatus.SERVICE_UNAVAILABLE.value()))
.andExpect(jsonPath("$.data.code").value("IAMPORT_API_UNAVAILABLE"))
.andExpect(
jsonPath("$.data.message")
.value("결제 대행 시스템(Iamport)과의 통신에 실패했습니다. 잠시 후 다시 시도해주세요."));
}
}

@Nested
class 앨범과_연결되지_않은_완료된_결제_내역_조회_요청_시 {

Expand Down
Loading