Skip to content

Commit

Permalink
feat: 토스페이먼츠 결제 API 연동 설정 (#466)
Browse files Browse the repository at this point in the history
* feat: 결제 시크릿 프로퍼티 추가

* chore: openfeign 의존성 추가

* chore: openfeign 세팅

* feat: 결제 승인 클라이언트 임시 구현

* chore: 환경변수 이름 수정

* feat: 토스페이먼츠 헤더 설정 추가

* feat: 주문 완료하기 API 추가

* refactor: feign 패키지 아래로 이동

* feat: feign 로그 설정

* style: 개행 제거

* fix: mysql 예약어 order로 인해 테이블 생성되지 않는 문제 해결

* feat: 시간 관련 포매팅 설정

* feat: ZonedDateTime으로 변경

* refactor: COMPLETED로 변경

* feat: 주문 완료하기 임시 구현

* fix: orders 테이블명으로 인한 불일치 수정

* docs: 투두 주석 추가
  • Loading branch information
uwoobeat authored Jul 12, 2024
1 parent 973e41a commit 794bb85
Show file tree
Hide file tree
Showing 15 changed files with 228 additions and 6 deletions.
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ ext {
set('snippetsDir', file("build/generated-snippets"))
}

dependencyManagement {
imports {
mavenBom 'org.springframework.cloud:spring-cloud-dependencies:2023.0.2'
}
}


dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
Expand Down Expand Up @@ -80,6 +87,9 @@ dependencies {

// Monitoring
implementation 'io.micrometer:micrometer-registry-prometheus'

// OpenFeign
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.gdschongik.gdsc.domain.order.api;

import com.gdschongik.gdsc.domain.order.application.OrderService;
import com.gdschongik.gdsc.domain.order.dto.request.OrderCompleteRequest;
import com.gdschongik.gdsc.domain.order.dto.request.OrderCreateRequest;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -26,4 +28,11 @@ public ResponseEntity<Void> createPendingOrder(@Valid @RequestBody OrderCreateRe
orderService.createPendingOrder(request);
return ResponseEntity.ok().build();
}

@Operation(summary = "주문 완료하기", description = "주문을 완료합니다. 요청된 결제는 승인됩니다.")
@PostMapping("/{orderId}/complete")
public ResponseEntity<Void> completeOrder(
@PathVariable Long orderId, @Valid @RequestBody OrderCompleteRequest request) {
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,22 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Comment;

@Getter
@Table(name = "orders")
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Order extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
@Column(name = "orders_id")
private Long id;

@Comment("주문상태")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

public enum OrderStatus {
PENDING,
COMPLETE,
COMPLETED,
CANCELED,
;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.gdschongik.gdsc.domain.order.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;

public record OrderCompleteRequest(
@NotBlank String paymentKey, @NotBlank @Size(min = 21, max = 21) String orderNanoId, @Positive Long amount) {}
27 changes: 27 additions & 0 deletions src/main/java/com/gdschongik/gdsc/global/config/FeignConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.gdschongik.gdsc.global.config;

import feign.Logger;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignFormatterRegistrar;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;

@Configuration
@EnableFeignClients("com.gdschongik.gdsc.infra")
public class FeignConfig {

@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}

@Bean
public FeignFormatterRegistrar dateTimeFormatterRegistrar() {
return registry -> {
var registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.gdschongik.gdsc.global.property.DiscordProperty;
import com.gdschongik.gdsc.global.property.EmailProperty;
import com.gdschongik.gdsc.global.property.JwtProperty;
import com.gdschongik.gdsc.global.property.PaymentProperty;
import com.gdschongik.gdsc.global.property.RedisProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
Expand All @@ -13,7 +14,8 @@
RedisProperty.class,
BasicAuthProperty.class,
DiscordProperty.class,
EmailProperty.class
EmailProperty.class,
PaymentProperty.class
})
@Configuration
public class PropertyConfig {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.gdschongik.gdsc.global.property;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Getter
@AllArgsConstructor
@ConfigurationProperties(prefix = "toss")
public class PaymentProperty {
private final String secretKey;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.gdschongik.gdsc.infra.feign.payment.client;

import com.gdschongik.gdsc.infra.feign.payment.config.BasicAuthConfig;
import com.gdschongik.gdsc.infra.feign.payment.dto.request.PaymentConfirmRequest;
import com.gdschongik.gdsc.infra.feign.payment.dto.response.PaymentResponse;
import jakarta.validation.Valid;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(name = "paymentClient", url = "https://api.tosspayments.com", configuration = BasicAuthConfig.class)
public interface PaymentClient {

@PostMapping("/v1/payments/confirm")
PaymentResponse confirm(@Valid @RequestBody PaymentConfirmRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.gdschongik.gdsc.infra.feign.payment.config;

import com.gdschongik.gdsc.global.property.PaymentProperty;
import feign.auth.BasicAuthRequestInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;

@RequiredArgsConstructor
public class BasicAuthConfig {

private final PaymentProperty paymentProperty;

@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor(paymentProperty.getSecretKey(), "");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.gdschongik.gdsc.infra.feign.payment.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;

public record PaymentConfirmRequest(
@NotBlank String paymentKey, @NotBlank @Size(min = 21, max = 21) String orderId, @Positive Long amount) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.gdschongik.gdsc.infra.feign.payment.dto.response;

import jakarta.annotation.Nullable;
import java.time.ZonedDateTime;
import java.util.List;

public record PaymentResponse(
String version,
String paymentKey,
String type,
String orderId,
String orderName,
String mId,
String currency,
String method,
Long totalAmount,
Long balanceAmount,
String status,
ZonedDateTime requestedAt,
ZonedDateTime approvedAt,
Boolean useEscrow,
@Nullable String lastTransactionKey,
Long suppliedAmount,
Long vat,
Boolean cultureExpense,
Long taxFreeAmount,
Long taxExemtionAmount,
@Nullable List<PaymentCancelDto> cancels,
Boolean isPartialCancelable,
@Nullable CardDto card,
@Nullable TransferDto transfer,
@Nullable ReceiptDto receipt,
@Nullable CheckoutDto checkout,
@Nullable EasyPayDto easyPay,
String country,
@Nullable FailureDto failure,
@Nullable CashReceiptDto cashReceipt,
@Nullable List<CashReceiptsDto> cashReceipts) {
// TODO: enum 관련 매핑 여부 검토
public record PaymentCancelDto(
Long cancelAmount,
String cancelReason,
Long taxFreeAmount,
Long refundableAmount,
Long easyPayDiscountAmount,
ZonedDateTime canceledAt,
String transactionKey,
@Nullable String receiptKey,
String cancelStatus,
@Nullable String cancelRequestId) {}

public record CardDto(
Long amount,
String issuerCode,
@Nullable String acquirerCode,
String number,
Integer installmentPlanMonths,
String approveNo,
Boolean useCardPoint,
String cardType,
String ownerType,
String acquireStatus,
Boolean isInterestFree,
@Nullable String interestPayer) {}

public record TransferDto(String bankCode, String settlementStatus) {}

public record ReceiptDto(String url) {}

public record CheckoutDto(String url) {}

public record EasyPayDto(String provider, Long amount, Long discountAmount) {}

public record FailureDto(String code, String message) {}

public record CashReceiptDto(
String type,
String receiptKey,
String issueNumber,
String receiptUrl,
Long amount,
Long taxFreeAmount,
Long taxExemptionAmount) {}

public record CashReceiptsDto(
String receiptKey,
String orderId,
String orderName,
String type,
String issueNumber,
String receiptUrl,
String businessNumber,
String transactionType,
Integer amount,
Integer taxFreeAmount,
String issueStatus,
Object failure,
String customerIdentityNumber,
ZonedDateTime requestedAt) {}
}
2 changes: 2 additions & 0 deletions src/main/resources/application-payment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
toss:
secret-key: ${PAYMENT_TOSS_SECRET_KEY:}
2 changes: 2 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ spring:
- actuator
- discord
- email
- payment

logging:
level:
com.gdschongik.gdsc.domain.*.api.*: debug
com.gdschongik.gdsc.infra.feign: debug
13 changes: 10 additions & 3 deletions src/test/java/com/gdschongik/gdsc/helper/DatabaseCleaner.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,22 @@ public void afterPropertiesSet() {

private void extractTableNames(Connection conn) {
tableNames = em.getMetamodel().getEntities().stream()
.map(EntityType::getName)
.map(this::convertCamelCaseToSnakeCase)
.map(DatabaseCleaner::getTableName)
.toList();
}

private static String getTableName(EntityType<?> entity) {
// TODO: 임시로 Order 테이블만 orders로 변환하도록 처리함. 추후 다른 테이블도 처리해야 함.
if (entity.getName().equals("Order")) {
return "orders";
}
return convertCamelCaseToSnakeCase(entity.getName());
}

/**
* 카멜 케이스로 되어있는 엔티티 이름을 스네이크 케이스로 되어있는 테이블 이름으로 변환한다.
*/
private String convertCamelCaseToSnakeCase(String name) {
private static String convertCamelCaseToSnakeCase(String name) {
return name.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
}

Expand Down

0 comments on commit 794bb85

Please sign in to comment.