From 52c4924f1a155938fab1d0810f73c708ae6976c4 Mon Sep 17 00:00:00 2001 From: binarywoo27 Date: Thu, 23 Nov 2023 10:49:09 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20KakaopayApproveRequestDto=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../request/KakaopayApproveRequestDto.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/kr/bb/payment/dto/request/KakaopayApproveRequestDto.java diff --git a/src/main/java/kr/bb/payment/dto/request/KakaopayApproveRequestDto.java b/src/main/java/kr/bb/payment/dto/request/KakaopayApproveRequestDto.java new file mode 100644 index 0000000..90cb615 --- /dev/null +++ b/src/main/java/kr/bb/payment/dto/request/KakaopayApproveRequestDto.java @@ -0,0 +1,19 @@ +package kr.bb.payment.dto.request; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class KakaopayApproveRequestDto { + private Long orderId; + private Long userId; + private String orderType; // 배송/픽업 판별용 + private String tid; + private String pgToken; +} From 7a1797aef27a9f33f66ecda6aee831661d7fe595 Mon Sep 17 00:00:00 2001 From: binarywoo27 Date: Fri, 24 Nov 2023 14:03:31 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=EA=B2=B0=EC=A0=9C=EC=8A=B9?= =?UTF-8?q?=EC=9D=B8=20API=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../bb/payment/config/RestTemplateConfig.java | 13 +++ .../OrderClientController.java | 19 +++- .../request/KakaopayApproveRequestDto.java | 1 - .../kr/bb/payment/dto/response/Amount.java | 30 ++++++ .../response/KakaoPayApproveResponseDto.java | 37 +++++++ .../java/kr/bb/payment/entity/Payment.java | 4 + .../payment/repository/PaymentRepository.java | 4 +- ...ReadyService.java => KakaopayService.java} | 31 +++++- .../kr/bb/payment/service/PaymentService.java | 26 +++++ .../payment/service/KakaopayApproveTest.java | 100 ++++++++++++++++++ ...erviceTest.java => KakaopayReadyTest.java} | 26 +++-- 12 files changed, 274 insertions(+), 18 deletions(-) create mode 100644 src/main/java/kr/bb/payment/config/RestTemplateConfig.java create mode 100644 src/main/java/kr/bb/payment/dto/response/Amount.java create mode 100644 src/main/java/kr/bb/payment/dto/response/KakaoPayApproveResponseDto.java rename src/main/java/kr/bb/payment/service/{KakaopayReadyService.java => KakaopayService.java} (64%) create mode 100644 src/test/java/kr/bb/payment/service/KakaopayApproveTest.java rename src/test/java/kr/bb/payment/service/{PaymentServiceTest.java => KakaopayReadyTest.java} (63%) diff --git a/build.gradle b/build.gradle index b3414a7..71fd3a5 100644 --- a/build.gradle +++ b/build.gradle @@ -40,6 +40,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation group: 'io.github.lotteon-maven', name: 'blooming-blooms-utils', version: '0.1.0-alpha2' runtimeOnly 'com.h2database:h2' + testImplementation 'org.mock-server:mockserver-netty:5.11.2' // 사용 중인 MockServer 버전 } dependencyManagement { diff --git a/src/main/java/kr/bb/payment/config/RestTemplateConfig.java b/src/main/java/kr/bb/payment/config/RestTemplateConfig.java new file mode 100644 index 0000000..354c472 --- /dev/null +++ b/src/main/java/kr/bb/payment/config/RestTemplateConfig.java @@ -0,0 +1,13 @@ +package kr.bb.payment.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + @Bean + public RestTemplate restTemplate(){ + return new RestTemplate(); + } +} diff --git a/src/main/java/kr/bb/payment/controller/clientcontroller/OrderClientController.java b/src/main/java/kr/bb/payment/controller/clientcontroller/OrderClientController.java index 3040ae9..a83c773 100644 --- a/src/main/java/kr/bb/payment/controller/clientcontroller/OrderClientController.java +++ b/src/main/java/kr/bb/payment/controller/clientcontroller/OrderClientController.java @@ -1,9 +1,10 @@ package kr.bb.payment.controller.clientcontroller; import bloomingblooms.response.SuccessResponse; +import kr.bb.payment.dto.request.KakaopayApproveRequestDto; import kr.bb.payment.dto.request.KakaopayReadyRequestDto; import kr.bb.payment.dto.response.KakaopayReadyResponseDto; -import kr.bb.payment.service.KakaopayReadyService; +import kr.bb.payment.service.KakaopayService; import kr.bb.payment.service.PaymentService; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -11,19 +12,20 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; @RestController @RequiredArgsConstructor public class OrderClientController { private final PaymentService paymentService; - private final KakaopayReadyService kakaopayReadyService; + private final KakaopayService kakaopayService; @PostMapping("/ready") public ResponseEntity> payReady( @RequestBody KakaopayReadyRequestDto readyRequestDto) { - KakaopayReadyResponseDto responseDto = kakaopayReadyService.kakaoPayReady(readyRequestDto); + KakaopayReadyResponseDto responseDto = kakaopayService.kakaoPayReady(readyRequestDto); return ResponseEntity.ok() .body( @@ -33,4 +35,15 @@ public ResponseEntity> payReady( .data(responseDto) .build()); } + + @PostMapping("/approve") + public ResponseEntity> payApprove(@RequestBody KakaopayApproveRequestDto approveRequestDto){ + + kakaopayService.kakaoPayApprove(approveRequestDto); + + return ResponseEntity.ok().body(SuccessResponse.builder() + .code(String.valueOf(HttpStatus.OK.value())) + .message(HttpStatus.OK.name()) + .build()); + } } diff --git a/src/main/java/kr/bb/payment/dto/request/KakaopayApproveRequestDto.java b/src/main/java/kr/bb/payment/dto/request/KakaopayApproveRequestDto.java index 90cb615..78b6ca6 100644 --- a/src/main/java/kr/bb/payment/dto/request/KakaopayApproveRequestDto.java +++ b/src/main/java/kr/bb/payment/dto/request/KakaopayApproveRequestDto.java @@ -13,7 +13,6 @@ public class KakaopayApproveRequestDto { private Long orderId; private Long userId; - private String orderType; // 배송/픽업 판별용 private String tid; private String pgToken; } diff --git a/src/main/java/kr/bb/payment/dto/response/Amount.java b/src/main/java/kr/bb/payment/dto/response/Amount.java new file mode 100644 index 0000000..feabbca --- /dev/null +++ b/src/main/java/kr/bb/payment/dto/response/Amount.java @@ -0,0 +1,30 @@ +package kr.bb.payment.dto.response; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@NoArgsConstructor +public class Amount { + + private Integer total; + private Integer tax_free; + private Integer vat; + private Integer point; + private Integer discount; + + public Amount(Integer total, Integer tax_free, Integer vat, Integer point, Integer discount) { + this.total = total; + this.tax_free = tax_free; + this.vat = vat; + this.point = point; + this.discount = discount; + } +} diff --git a/src/main/java/kr/bb/payment/dto/response/KakaoPayApproveResponseDto.java b/src/main/java/kr/bb/payment/dto/response/KakaoPayApproveResponseDto.java new file mode 100644 index 0000000..12ef7cb --- /dev/null +++ b/src/main/java/kr/bb/payment/dto/response/KakaoPayApproveResponseDto.java @@ -0,0 +1,37 @@ +package kr.bb.payment.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Getter +@Setter +@ToString +@Builder +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class KakaoPayApproveResponseDto { + private String aid; // 요청 고유번호 + private String tid; // 결제 고유번호 + private String cid; // 가맹점 코드 + private String sid; // 정기 결제용 id + @JsonProperty("partner_order_id") + private String partnerOrderId; // 가맹점 주문번호 + @JsonProperty("partner_user_id") + private String partnerUserId; // 가맹점 회원 + @JsonProperty("payment_method_type") + private String paymentMethodType; // 결제수단 + @JsonProperty("item_name") + private String itemName; + private Integer quantity; + @JsonProperty("created_at") + private String createdAt; + @JsonProperty("approved_at") + private String approvedAt; + private Amount amount; +} diff --git a/src/main/java/kr/bb/payment/entity/Payment.java b/src/main/java/kr/bb/payment/entity/Payment.java index ed02cd1..adc821c 100644 --- a/src/main/java/kr/bb/payment/entity/Payment.java +++ b/src/main/java/kr/bb/payment/entity/Payment.java @@ -12,9 +12,13 @@ import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; +import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; @Entity +@Getter +@Setter @Table(name = "payment") @AllArgsConstructor(access = AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PROTECTED) diff --git a/src/main/java/kr/bb/payment/repository/PaymentRepository.java b/src/main/java/kr/bb/payment/repository/PaymentRepository.java index 2cb9a06..b9198b7 100644 --- a/src/main/java/kr/bb/payment/repository/PaymentRepository.java +++ b/src/main/java/kr/bb/payment/repository/PaymentRepository.java @@ -3,4 +3,6 @@ import kr.bb.payment.entity.Payment; import org.springframework.data.jpa.repository.JpaRepository; -public interface PaymentRepository extends JpaRepository {} +public interface PaymentRepository extends JpaRepository { + Payment findByOrderId(Long orderId); +} diff --git a/src/main/java/kr/bb/payment/service/KakaopayReadyService.java b/src/main/java/kr/bb/payment/service/KakaopayService.java similarity index 64% rename from src/main/java/kr/bb/payment/service/KakaopayReadyService.java rename to src/main/java/kr/bb/payment/service/KakaopayService.java index 7dfe140..139ee7a 100644 --- a/src/main/java/kr/bb/payment/service/KakaopayReadyService.java +++ b/src/main/java/kr/bb/payment/service/KakaopayService.java @@ -1,7 +1,10 @@ package kr.bb.payment.service; +import kr.bb.payment.dto.request.KakaopayApproveRequestDto; import kr.bb.payment.dto.request.KakaopayReadyRequestDto; +import kr.bb.payment.dto.response.KakaoPayApproveResponseDto; import kr.bb.payment.dto.response.KakaopayReadyResponseDto; +import kr.bb.payment.entity.Payment; import lombok.RequiredArgsConstructor; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Value; @@ -14,8 +17,9 @@ @RequiredArgsConstructor @Component -public class KakaopayReadyService { +public class KakaopayService { private final PaymentService paymentService; + private final RestTemplate restTemplate; @Value("${kakao.admin}") private String ADMIN_KEY; @@ -45,16 +49,37 @@ public KakaopayReadyResponseDto kakaoPayReady(KakaopayReadyRequestDto requestDto HttpEntity> requestEntity = new HttpEntity<>(parameters, this.getHeaders()); - RestTemplate template = new RestTemplate(); String url = "https://kapi.kakao.com/v1/payment/ready"; KakaopayReadyResponseDto responseDto = - template.postForObject(url, requestEntity, KakaopayReadyResponseDto.class); + restTemplate.postForObject(url, requestEntity, KakaopayReadyResponseDto.class); paymentService.savePayReadyInfo(requestDto, responseDto, cid); return responseDto; } + public void kakaoPayApprove(KakaopayApproveRequestDto requestDto) { + Payment paymentEntity = paymentService.getPaymentEntity(requestDto.getOrderId()); + + MultiValueMap parameters = new LinkedMultiValueMap<>(); + + parameters.add("cid", paymentEntity.getPaymentCid()); + parameters.add("tid", requestDto.getTid()); + parameters.add("partner_order_id", String.valueOf(requestDto.getOrderId())); + parameters.add("partner_user_id", String.valueOf(requestDto.getUserId())); + parameters.add("pg_token", requestDto.getPgToken()); + + HttpEntity> requestEntity = + new HttpEntity<>(parameters, this.getHeaders()); + + String url = "https://kapi.kakao.com/v1/payment/approve"; + + KakaoPayApproveResponseDto approveResponse = + restTemplate.postForObject(url, requestEntity, KakaoPayApproveResponseDto.class); + + paymentService.updatePayInfo(paymentEntity, approveResponse); + } + @NotNull private HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); diff --git a/src/main/java/kr/bb/payment/service/PaymentService.java b/src/main/java/kr/bb/payment/service/PaymentService.java index a200229..055aab7 100644 --- a/src/main/java/kr/bb/payment/service/PaymentService.java +++ b/src/main/java/kr/bb/payment/service/PaymentService.java @@ -1,6 +1,7 @@ package kr.bb.payment.service; import kr.bb.payment.dto.request.KakaopayReadyRequestDto; +import kr.bb.payment.dto.response.KakaoPayApproveResponseDto; import kr.bb.payment.dto.response.KakaopayReadyResponseDto; import kr.bb.payment.entity.OrderType; import kr.bb.payment.entity.Payment; @@ -47,4 +48,29 @@ public KakaopayReadyResponseDto savePayReadyInfo( return responseDto; } + + /** + * orderId로 Payment Entity 찾기 + * + * @param orderId + * @return + */ + @Transactional + public Payment getPaymentEntity(Long orderId) { + return paymentRepository.findByOrderId(orderId); + } + + /** + * 결제수단, 결제상태 업데이트(PENDING -> COMPLETED) + * + * @param paymentEntity + * @param approveResponse + */ + @Transactional + public void updatePayInfo(Payment paymentEntity, KakaoPayApproveResponseDto approveResponse) { + paymentEntity.setPaymentType(approveResponse.getPaymentMethodType()); + paymentEntity.setPaymentStatus(PaymentStatus.COMPLETED); + + paymentRepository.save(paymentEntity); + } } diff --git a/src/test/java/kr/bb/payment/service/KakaopayApproveTest.java b/src/test/java/kr/bb/payment/service/KakaopayApproveTest.java new file mode 100644 index 0000000..eecbfa0 --- /dev/null +++ b/src/test/java/kr/bb/payment/service/KakaopayApproveTest.java @@ -0,0 +1,100 @@ +package kr.bb.payment.service; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import kr.bb.payment.dto.request.KakaopayApproveRequestDto; +import kr.bb.payment.dto.response.Amount; +import kr.bb.payment.dto.response.KakaoPayApproveResponseDto; +import kr.bb.payment.entity.OrderType; +import kr.bb.payment.entity.Payment; +import kr.bb.payment.entity.PaymentStatus; +import kr.bb.payment.repository.PaymentRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.test.web.client.match.MockRestRequestMatchers; +import org.springframework.test.web.client.response.MockRestResponseCreators; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +@SpringBootTest +@Transactional +public class KakaopayApproveTest { + @Autowired + private RestTemplate restTemplate; + private MockRestServiceServer mockServer; + @Autowired + private KakaopayService kakaopayService; + @Autowired + private PaymentRepository paymentRepository; + + @BeforeEach + void setUp() throws Exception{ + mockServer = MockRestServiceServer.createServer(restTemplate); + + // Payment Entity 저장 + Payment payment = + Payment.builder() + .userId(1L) + .orderId(1L) + .orderType(OrderType.ORDER_DELIVERY) + .paymentCid("TC0ONETIME") + .paymentTid("FAKE_TID_FOR_TEST") + .paymentActualAmount(52900L) + .paymentStatus(PaymentStatus.PENDING) + .build(); + paymentRepository.save(payment); + + KakaoPayApproveResponseDto responseDto = + KakaoPayApproveResponseDto.builder() + .aid("A5678901234567890123") + .tid("T1234567890123456789") + .cid("TC0ONETIME") + .partnerOrderId("partner_order_id") + .partnerUserId("partner_user_id") + .paymentMethodType("MONEY") + .itemName("초코파이") + .quantity(1) + .amount(new Amount(2200, 0, 200, 0, 0)) + .createdAt("2016-11-15T21:18:22") + .approvedAt("2016-11-15T21:20:47") + .build(); + + ObjectMapper objectMapper = new ObjectMapper(); + String responseJson = objectMapper.writeValueAsString(responseDto); + + mockServer + .expect(MockRestRequestMatchers.requestTo("https://kapi.kakao.com/v1/payment/approve")) + .andExpect(MockRestRequestMatchers.method(HttpMethod.POST)) + .andRespond(MockRestResponseCreators.withSuccess(responseJson, MediaType.APPLICATION_JSON)); + } + + @AfterEach + void shutDown() { + mockServer.reset(); + } + + @DisplayName("결제 승인 테스트") + @DirtiesContext + @Test + void kakaoPayApproveTest() { + KakaopayApproveRequestDto requestDto = + KakaopayApproveRequestDto.builder() + .userId(1L) + .orderId(1L) + .tid("T1234567890123456789") + .pgToken("pg_token=xxxxxxxxxxxxxxxxxxxx") + .build(); + + kakaopayService.kakaoPayApprove(requestDto); + + mockServer.verify(); + } +} diff --git a/src/test/java/kr/bb/payment/service/PaymentServiceTest.java b/src/test/java/kr/bb/payment/service/KakaopayReadyTest.java similarity index 63% rename from src/test/java/kr/bb/payment/service/PaymentServiceTest.java rename to src/test/java/kr/bb/payment/service/KakaopayReadyTest.java index 7507903..df226ca 100644 --- a/src/test/java/kr/bb/payment/service/PaymentServiceTest.java +++ b/src/test/java/kr/bb/payment/service/KakaopayReadyTest.java @@ -7,17 +7,21 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.transaction.annotation.Transactional; @SpringBootTest -public class PaymentServiceTest { - @Autowired - private KakaopayReadyService kakaopayReadyService; +@Transactional +public class KakaopayReadyTest { + @Autowired private KakaopayService kakaopayService; @DisplayName("단건결제 준비 - 픽업") + @DirtiesContext @Test void kakaopayReadyForDeliveryAndPickupTest() { // given - KakaopayReadyRequestDto DTO_1 = KakaopayReadyRequestDto.builder() + KakaopayReadyRequestDto DTO_1 = + KakaopayReadyRequestDto.builder() .userId("1") .orderId("1") .orderType("ORDER_PICKUP") @@ -29,16 +33,18 @@ void kakaopayReadyForDeliveryAndPickupTest() { .build(); // then - KakaopayReadyResponseDto responseDto1 = kakaopayReadyService.kakaoPayReady(DTO_1); - Assertions.assertEquals(20, responseDto1.getTid().length()); - Assertions.assertTrue(responseDto1.getNextRedirectPcUrl().startsWith("https://")); + KakaopayReadyResponseDto responseDto = kakaopayService.kakaoPayReady(DTO_1); + Assertions.assertEquals(20, responseDto.getTid().length()); + Assertions.assertTrue(responseDto.getNextRedirectPcUrl().startsWith("https://")); } @DisplayName("단건결제 준비 - 배송&구독") + @DirtiesContext @Test - void kakaopayReadyForSubscriptionTest(){ + void kakaopayReadyForSubscriptionTest() { // given - KakaopayReadyRequestDto DTO_2 = KakaopayReadyRequestDto.builder() + KakaopayReadyRequestDto DTO_2 = + KakaopayReadyRequestDto.builder() .userId("1") .orderId("2") .orderType("ORDER_DELIVERY") @@ -50,7 +56,7 @@ void kakaopayReadyForSubscriptionTest(){ .build(); // then - KakaopayReadyResponseDto responseDto2 = kakaopayReadyService.kakaoPayReady(DTO_2); + KakaopayReadyResponseDto responseDto2 = kakaopayService.kakaoPayReady(DTO_2); Assertions.assertEquals(20, responseDto2.getTid().length()); Assertions.assertTrue(responseDto2.getNextRedirectPcUrl().startsWith("https://")); }