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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,7 @@ src/main/resources/application.yml
build.gradle

# QueryDSL generated sources
/build/generated/
/build/generated/

src/main/resources/static/payment-test.html
src/main/resources/static/payment-success.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.eatsfine.eatsfine.domain.payment.controller;

import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentConfirmDTO;
import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentRequestDTO;
import com.eatsfine.eatsfine.domain.payment.dto.response.PaymentResponseDTO;
import com.eatsfine.eatsfine.domain.payment.service.PaymentService;
Expand All @@ -21,10 +22,17 @@ public class PaymentController {

private final PaymentService paymentService;

@Operation(summary = "๊ฒฐ์ œ ์š”์ฒญ", description = "์˜ˆ์•ฝ ID์™€ ๊ฒฐ์ œ ์ œ๊ณต์ž๋ฅผ ๋ฐ›์•„ ๊ฒฐ์ œ๋ฅผ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.")
@Operation(summary = "๊ฒฐ์ œ ์š”์ฒญ", description = "์˜ˆ์•ฝ ID๋ฅผ ๋ฐ›์•„ ์ฃผ๋ฌธ ID๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ฒฐ์ œ ์ •๋ณด๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.")
@PostMapping("/request")
public ApiResponse<PaymentResponseDTO.PaymentRequestResultDTO> requestPayment(
@RequestBody @Valid PaymentRequestDTO.RequestPaymentDTO dto) {
return ApiResponse.onSuccess(paymentService.requestPayment(dto));
}

@Operation(summary = "๊ฒฐ์ œ ์Šน์ธ", description = "ํ† ์ŠคํŽ˜์ด๋จผ์ธ  ๊ฒฐ์ œ ์Šน์ธ์„ ์š”์ฒญํ•ฉ๋‹ˆ๋‹ค.")
@PostMapping("/confirm")
public ApiResponse<PaymentResponseDTO.PaymentRequestResultDTO> confirmPayment(
@RequestBody @Valid PaymentConfirmDTO dto) {
return ApiResponse.onSuccess(paymentService.confirmPayment(dto));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.eatsfine.eatsfine.domain.payment.dto.request;

import jakarta.validation.constraints.NotNull;
import lombok.Builder;

@Builder
public record PaymentConfirmDTO(
@NotNull String paymentKey,
@NotNull String orderId,
@NotNull Integer amount) {
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
package com.eatsfine.eatsfine.domain.payment.dto.request;

import com.eatsfine.eatsfine.domain.payment.enums.PaymentProvider;
import jakarta.validation.constraints.NotNull;

public class PaymentRequestDTO {

public record RequestPaymentDTO(
@NotNull Long bookingId,
@NotNull PaymentProvider provider,
@NotNull String successUrl,
@NotNull String failUrl) {
@NotNull Long bookingId) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ public class PaymentResponseDTO {
public record PaymentRequestResultDTO(
Long paymentId,
Long bookingId,
PaymentMethod paymentMethod,
String tid,
String orderId,
Integer amount,
PaymentStatus paymentStatus,
String nextRedirectUrl,
LocalDateTime requestedAt) {
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.eatsfine.eatsfine.domain.payment.dto.response;

import java.time.OffsetDateTime;

public record TossPaymentResponse(
String paymentKey,
String type,
String orderId,
String orderName,
String mId,
String currency,
String method,
Integer totalAmount,
Integer balanceAmount,
String status,
OffsetDateTime requestedAt,
OffsetDateTime approvedAt,
Boolean useEscrow,
String lastTransactionKey,
Integer suppliedAmount,
Integer vat,
EasyPay easyPay) {

public record EasyPay(
String provider,
Integer amount,
Integer discountAmount) {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public class Payment extends BaseEntity {
private String paymentKey;

@Enumerated(EnumType.STRING)
@Column(name = "payment_provider", nullable = false)
@Column(name = "payment_provider")
private PaymentProvider paymentProvider;

@Enumerated(EnumType.STRING)
Expand All @@ -63,10 +63,12 @@ public void setPaymentKey(String paymentKey) {
this.paymentKey = paymentKey;
}

public void completePayment(LocalDateTime approvedAt, PaymentMethod method, String paymentKey) {
public void completePayment(LocalDateTime approvedAt, PaymentMethod method, String paymentKey,
PaymentProvider provider) {
this.paymentStatus = PaymentStatus.COMPLETED;
this.approvedAt = approvedAt;
this.paymentMethod = method;
this.paymentKey = paymentKey;
this.paymentProvider = provider;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
@Getter
@AllArgsConstructor
public enum PaymentMethod {
CARD("์นด๋“œ"),
VIRTUAL_ACCOUNT("๊ฐ€์ƒ๊ณ„์ขŒ"),
SIMPLE_PAYMENT("๊ฐ„ํŽธ๊ฒฐ์ œ"),
PHONE("ํœด๋Œ€ํฐ");
SIMPLE_PAYMENT("๊ฐ„ํŽธ๊ฒฐ์ œ");

private final String description;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@
import com.eatsfine.eatsfine.domain.payment.entity.Payment;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface PaymentRepository extends JpaRepository<Payment, Long> {
Optional<Payment> findByOrderId(String orderId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

import com.eatsfine.eatsfine.domain.booking.entity.Booking;
import com.eatsfine.eatsfine.domain.booking.repository.BookingRepository;
import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus;
import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException;
import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentConfirmDTO;
import com.eatsfine.eatsfine.domain.payment.dto.request.PaymentRequestDTO;
import com.eatsfine.eatsfine.domain.payment.dto.response.PaymentResponseDTO;
import com.eatsfine.eatsfine.domain.payment.dto.response.TossPaymentResponse;
import com.eatsfine.eatsfine.domain.payment.entity.Payment;

import com.eatsfine.eatsfine.domain.payment.enums.PaymentMethod;
import com.eatsfine.eatsfine.domain.payment.enums.PaymentProvider;
import com.eatsfine.eatsfine.domain.payment.enums.PaymentStatus;
import com.eatsfine.eatsfine.domain.payment.enums.PaymentType;
import com.eatsfine.eatsfine.domain.payment.repository.PaymentRepository;
import com.eatsfine.eatsfine.global.apiPayload.code.status.ErrorStatus;
import com.eatsfine.eatsfine.global.apiPayload.exception.GeneralException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestClient;

import java.time.LocalDateTime;
import java.util.UUID;
Expand All @@ -22,46 +26,85 @@
@RequiredArgsConstructor
public class PaymentService {

private final PaymentRepository paymentRepository;
private final BookingRepository bookingRepository;

@Transactional
public PaymentResponseDTO.PaymentRequestResultDTO requestPayment(PaymentRequestDTO.RequestPaymentDTO dto) {
Booking booking = bookingRepository.findById(dto.bookingId())
.orElseThrow(() -> new IllegalArgumentException("Booking not found"));

// ์ฃผ๋ฌธ ID ์ƒ์„ฑ
String orderId = UUID.randomUUID().toString();

// ์˜ˆ์•ฝ๊ธˆ ๊ฒ€์ฆ
if (booking.getDepositAmount() == null || booking.getDepositAmount() <= 0) {
throw new GeneralException(ErrorStatus.PAYMENT_INVALID_DEPOSIT);
}

Payment payment = Payment.builder()
.booking(booking)
.orderId(orderId)
.amount(booking.getDepositAmount())
.paymentProvider(dto.provider())
.paymentStatus(PaymentStatus.PENDING)
.paymentType(PaymentType.DEPOSIT)
.requestedAt(LocalDateTime.now())
.build();

Payment savedPayment = paymentRepository.save(payment);

// ์™ธ๋ถ€ ๊ฒฐ์ œ ์ œ๊ณต์ž ์‘๋‹ต ๋ชจ์˜ ์ฒ˜๋ฆฌ
String tid = "T" + UUID.randomUUID().toString().replace("-", "").substring(0, 10);
String nextRedirectUrl = "https://mock.api.kakaopay.com/online/v1/payment/ready/" + tid;

return new PaymentResponseDTO.PaymentRequestResultDTO(
savedPayment.getId(),
booking.getId(),
savedPayment.getPaymentMethod(),
tid,
savedPayment.getAmount(),
savedPayment.getPaymentStatus(),
nextRedirectUrl,
savedPayment.getRequestedAt());
private final PaymentRepository paymentRepository;
private final BookingRepository bookingRepository;
private final RestClient tossPaymentClient;

@Transactional
public PaymentResponseDTO.PaymentRequestResultDTO requestPayment(PaymentRequestDTO.RequestPaymentDTO dto) {
Booking booking = bookingRepository.findById(dto.bookingId())
.orElseThrow(() -> new GeneralException(ErrorStatus.BOOKING_NOT_FOUND));

// ์ฃผ๋ฌธ ID ์ƒ์„ฑ
String orderId = UUID.randomUUID().toString();

// ์˜ˆ์•ฝ๊ธˆ ๊ฒ€์ฆ
if (booking.getDepositAmount() == null || booking.getDepositAmount() <= 0) {
throw new GeneralException(ErrorStatus.PAYMENT_INVALID_DEPOSIT);
}

Payment payment = Payment.builder()
.booking(booking)
.orderId(orderId)
.amount(booking.getDepositAmount())
.paymentStatus(PaymentStatus.PENDING)
.paymentType(PaymentType.DEPOSIT)
.requestedAt(LocalDateTime.now())
.build();

Payment savedPayment = paymentRepository.save(payment);

return new PaymentResponseDTO.PaymentRequestResultDTO(
savedPayment.getId(),
booking.getId(),
savedPayment.getOrderId(),
savedPayment.getAmount(),
savedPayment.getRequestedAt());
}

@Transactional
public PaymentResponseDTO.PaymentRequestResultDTO confirmPayment(PaymentConfirmDTO dto) {
Payment payment = paymentRepository.findByOrderId(dto.orderId())
.orElseThrow(() -> new GeneralException(ErrorStatus.PAYMENT_NOT_FOUND));

if (!payment.getAmount().equals(dto.amount())) {
throw new GeneralException(ErrorStatus.PAYMENT_INVALID_AMOUNT);
}

// ํ† ์Šค API ํ˜ธ์ถœ
TossPaymentResponse response = tossPaymentClient.post()
.uri("/v1/payments/confirm")
.body(dto)
.retrieve()
.body(TossPaymentResponse.class);

if (response == null || !"DONE".equals(response.status())) {
throw new GeneralException(ErrorStatus._INTERNAL_SERVER_ERROR);
}

// Provider ํŒŒ์‹ฑ
PaymentProvider provider = null;
if (response.easyPay() != null) {
String providerCode = response.easyPay().provider();
if ("ํ† ์ŠคํŽ˜์ด".equals(providerCode)) {
provider = PaymentProvider.TOSS;
} else if ("์นด์นด์˜คํŽ˜์ด".equals(providerCode)) {
provider = PaymentProvider.KAKAOPAY;
}
}

payment.completePayment(
response.approvedAt() != null ? response.approvedAt().toLocalDateTime() : LocalDateTime.now(),
PaymentMethod.SIMPLE_PAYMENT,
response.paymentKey(),
provider
);

return new PaymentResponseDTO.PaymentRequestResultDTO(
payment.getId(),
payment.getBooking().getId(),
payment.getOrderId(),
payment.getAmount(),
payment.getRequestedAt());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ public enum ErrorStatus implements BaseErrorCode {
_NOT_FOUND(HttpStatus.NOT_FOUND, "COMMON404", "์กด์žฌํ•˜์ง€ ์•Š๋Š” ์š”์ฒญ์ž…๋‹ˆ๋‹ค."),

// ์˜ˆ์•ฝ๊ธˆ ๊ด€๋ จ ์—๋Ÿฌ
PAYMENT_INVALID_DEPOSIT(HttpStatus.BAD_REQUEST, "PAYMENT4001", "์˜ˆ์•ฝ๊ธˆ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.");
PAYMENT_INVALID_DEPOSIT(HttpStatus.BAD_REQUEST, "PAYMENT4001", "์˜ˆ์•ฝ๊ธˆ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),
PAYMENT_INVALID_AMOUNT(HttpStatus.BAD_REQUEST, "PAYMENT4002", "๊ฒฐ์ œ ๊ธˆ์•ก์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."),
PAYMENT_NOT_FOUND(HttpStatus.NOT_FOUND, "PAYMENT4003", "๊ฒฐ์ œ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."),

// ์˜ˆ์•ฝ ๊ด€๋ จ ์—๋Ÿฌ
BOOKING_NOT_FOUND(HttpStatus.NOT_FOUND, "BOOKING4001", "์˜ˆ์•ฝ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.eatsfine.eatsfine.global.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;

import java.util.Base64;

@Configuration
public class TossPaymentConfig {

@Value("${payment.toss.widget-secret-key}")
private String widgetSecretKey;

@Bean
public RestClient tossPaymentClient() {
String encodedSecretKey = Base64.getEncoder().encodeToString((widgetSecretKey + ":").getBytes());

return RestClient.builder()
.baseUrl("https://api.tosspayments.com")
.defaultHeader("Authorization", "Basic " + encodedSecretKey)
.defaultHeader("Content-Type", "application/json")
.build();
}
}
4 changes: 4 additions & 0 deletions src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ spring:
properties:
hibernate:
format_sql: true

payment:
toss:
widget-secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6
21 changes: 17 additions & 4 deletions src/test/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
spring:
application:
name: Eatsfine
profiles:
active: test
datasource:
url: jdbc:h2:mem:testdb;MODE=MySQL
driver-class-name: org.h2.Driver
username:
password:
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.H2Dialect

payment:
toss:
widget-secret-key: test_sk_sample_key_for_testing