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 new file mode 100644 index 0000000..78b6ca6 --- /dev/null +++ b/src/main/java/kr/bb/payment/dto/request/KakaopayApproveRequestDto.java @@ -0,0 +1,18 @@ +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 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://")); }