generated from pricelees/issue-pr-template
<!-- 제목 양식 --> <!-- [이슈번호] 작업 요약 (예시: [#10] Gitea 템플릿 생성) --> ## 📝 관련 이슈 및 PR **PR과 관련된 이슈 번호** - #11 ## ✨ 작업 내용 <!-- 어떤 작업을 했는지 알려주세요! --> payment 패키지 내 코드, 테스트를 코틀린으로 전환했고 일부 로직은 개선하였음. 전체적으로 구조를 개선하려고 했으나, 얽혀있는 예약 관련 로직이 많아 전체 코드의 코틀린 전환이 끝난 이후 개선할 예정 ## 🧪 테스트 <!-- 어떤 테스트를 생각했고 진행했는지 알려주세요! --> 1. \@DataJpaTest를 이용하는 Repository 테스트를 추가 2. Service는 mocking 방식으로 수정하였고, 테스트가 불필요하다고 여겨지는 단순 로직(변환 또는 Repository만 사용하는 경우)은 제외하였음. (8577b68496) - 전체 로직이 테스트되어있는 기존의 테스트는 유지하였고, 전체 코틀린 전환이 마무리 된 후 제거 예정 ## 📚 참고 자료 및 기타 <!-- 참고한 자료, 또는 논의할 사항이 있다면 알려주세요! --> Reviewed-on: #12 Co-authored-by: pricelees <priceelees@gmail.com> Co-committed-by: pricelees <priceelees@gmail.com>
This commit is contained in:
parent
19da58c1f3
commit
ed383c3092
@ -32,7 +32,7 @@ enum class ErrorType(
|
||||
RESERVATION_NOT_FOUND("예약(Reservation) 정보가 존재하지 않습니다."),
|
||||
RESERVATION_TIME_NOT_FOUND("예약 시간(ReservationTime) 정보가 존재하지 않습니다."),
|
||||
THEME_NOT_FOUND("테마(Theme) 정보가 존재하지 않습니다."),
|
||||
PAYMENT_NOT_POUND("결제(Payment) 정보가 존재하지 않습니다."),
|
||||
PAYMENT_NOT_FOUND("결제(Payment) 정보가 존재하지 않습니다."),
|
||||
|
||||
// 405 Method Not Allowed
|
||||
METHOD_NOT_ALLOWED("지원하지 않는 HTTP Method 입니다."),
|
||||
@ -52,7 +52,8 @@ enum class ErrorType(
|
||||
|
||||
// Payment Error
|
||||
PAYMENT_ERROR("결제(취소)에 실패했습니다. 결제(취소) 정보를 확인해주세요."),
|
||||
PAYMENT_SERVER_ERROR("결제 서버에서 에러가 발생하였습니다. 잠시 후 다시 시도해주세요.");
|
||||
PAYMENT_SERVER_ERROR("결제 서버에서 에러가 발생하였습니다. 잠시 후 다시 시도해주세요.")
|
||||
;
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
package roomescape.payment;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.client.ClientHttpRequestFactories;
|
||||
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.ClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import roomescape.payment.client.PaymentProperties;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(PaymentProperties.class)
|
||||
public class PaymentConfig {
|
||||
|
||||
@Bean
|
||||
public RestClient.Builder restClientBuilder(PaymentProperties paymentProperties) {
|
||||
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS
|
||||
.withReadTimeout(Duration.ofSeconds(paymentProperties.getReadTimeout()))
|
||||
.withConnectTimeout(Duration.ofSeconds(paymentProperties.getConnectTimeout()));
|
||||
ClientHttpRequestFactory requestFactory = ClientHttpRequestFactories.get(settings);
|
||||
|
||||
return RestClient.builder().baseUrl("https://api.tosspayments.com")
|
||||
.defaultHeader("Authorization", getAuthorizations(paymentProperties.getConfirmSecretKey()))
|
||||
.requestFactory(requestFactory);
|
||||
}
|
||||
|
||||
private String getAuthorizations(String secretKey) {
|
||||
Base64.Encoder encoder = Base64.getEncoder();
|
||||
byte[] encodedBytes = encoder.encode((secretKey + ":").getBytes(StandardCharsets.UTF_8));
|
||||
return "Basic " + new String(encodedBytes);
|
||||
}
|
||||
}
|
||||
108
src/main/java/roomescape/payment/business/PaymentService.kt
Normal file
108
src/main/java/roomescape/payment/business/PaymentService.kt
Normal file
@ -0,0 +1,108 @@
|
||||
package roomescape.payment.business
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import roomescape.payment.infrastructure.persistence.PaymentRepository
|
||||
import roomescape.payment.web.PaymentApprove
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
import roomescape.payment.web.ReservationPaymentResponse
|
||||
import roomescape.payment.web.toReservationPaymentResponse
|
||||
import roomescape.reservation.domain.Reservation
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Service
|
||||
class PaymentService(
|
||||
private val paymentRepository: PaymentRepository,
|
||||
private val canceledPaymentRepository: CanceledPaymentRepository
|
||||
) {
|
||||
@Transactional
|
||||
fun savePayment(
|
||||
paymentResponse: PaymentApprove.Response,
|
||||
reservation: Reservation
|
||||
): ReservationPaymentResponse = PaymentEntity(
|
||||
orderId = paymentResponse.orderId,
|
||||
paymentKey = paymentResponse.paymentKey,
|
||||
totalAmount = paymentResponse.totalAmount,
|
||||
reservation = reservation,
|
||||
approvedAt = paymentResponse.approvedAt
|
||||
).also {
|
||||
paymentRepository.save(it)
|
||||
}.toReservationPaymentResponse()
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun isReservationPaid(
|
||||
reservationId: Long
|
||||
): Boolean = paymentRepository.existsByReservationId(reservationId)
|
||||
|
||||
@Transactional
|
||||
fun saveCanceledPayment(
|
||||
cancelInfo: PaymentCancel.Response,
|
||||
approvedAt: OffsetDateTime,
|
||||
paymentKey: String
|
||||
): CanceledPaymentEntity = CanceledPaymentEntity(
|
||||
paymentKey = paymentKey,
|
||||
cancelReason = cancelInfo.cancelReason,
|
||||
cancelAmount = cancelInfo.cancelAmount,
|
||||
approvedAt = approvedAt,
|
||||
canceledAt = cancelInfo.canceledAt
|
||||
).also { canceledPaymentRepository.save(it) }
|
||||
|
||||
|
||||
@Transactional
|
||||
fun cancelPaymentByAdmin(reservationId: Long): PaymentCancel.Request {
|
||||
val paymentKey: String = paymentRepository.findPaymentKeyByReservationId(reservationId)
|
||||
?: throw RoomescapeException(
|
||||
ErrorType.PAYMENT_NOT_FOUND,
|
||||
"[reservationId: $reservationId]",
|
||||
HttpStatus.NOT_FOUND
|
||||
)
|
||||
// 취소 시간은 현재 시간으로 일단 생성한 뒤, 결제 취소 완료 후 해당 시간으로 변경합니다.
|
||||
val canceled: CanceledPaymentEntity = cancelPayment(paymentKey)
|
||||
|
||||
return PaymentCancel.Request(paymentKey, canceled.cancelAmount, canceled.cancelReason)
|
||||
}
|
||||
|
||||
private fun cancelPayment(
|
||||
paymentKey: String,
|
||||
cancelReason: String = "고객 요청",
|
||||
canceledAt: OffsetDateTime = OffsetDateTime.now()
|
||||
): CanceledPaymentEntity {
|
||||
val paymentEntity: PaymentEntity = paymentRepository.findByPaymentKey(paymentKey)
|
||||
?.also { paymentRepository.delete(it) }
|
||||
?: throw RoomescapeException(
|
||||
ErrorType.PAYMENT_NOT_FOUND,
|
||||
"[paymentKey: $paymentKey]",
|
||||
HttpStatus.NOT_FOUND
|
||||
)
|
||||
|
||||
return CanceledPaymentEntity(
|
||||
paymentKey = paymentKey,
|
||||
cancelReason = cancelReason,
|
||||
cancelAmount = paymentEntity.totalAmount,
|
||||
approvedAt = paymentEntity.approvedAt,
|
||||
canceledAt = canceledAt
|
||||
).also {
|
||||
canceledPaymentRepository.save(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun updateCanceledTime(
|
||||
paymentKey: String,
|
||||
canceledAt: OffsetDateTime
|
||||
) {
|
||||
canceledPaymentRepository.findByPaymentKey(paymentKey)?.let {
|
||||
it.canceledAt = canceledAt
|
||||
} ?: throw RoomescapeException(
|
||||
ErrorType.PAYMENT_NOT_FOUND,
|
||||
"[paymentKey: $paymentKey]",
|
||||
HttpStatus.NOT_FOUND
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
package roomescape.payment.client;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ConfigurationProperties(prefix = "payment")
|
||||
public class PaymentProperties {
|
||||
|
||||
private final String confirmSecretKey;
|
||||
private final int readTimeout;
|
||||
private final int connectTimeout;
|
||||
|
||||
public PaymentProperties(String confirmSecretKey, int readTimeout, int connectTimeout) {
|
||||
this.confirmSecretKey = confirmSecretKey;
|
||||
this.readTimeout = readTimeout;
|
||||
this.connectTimeout = connectTimeout;
|
||||
}
|
||||
|
||||
public String getConfirmSecretKey() {
|
||||
return confirmSecretKey;
|
||||
}
|
||||
|
||||
public int getReadTimeout() {
|
||||
return readTimeout;
|
||||
}
|
||||
|
||||
public int getConnectTimeout() {
|
||||
return connectTimeout;
|
||||
}
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
package roomescape.payment.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.payment.dto.request.PaymentCancelRequest;
|
||||
import roomescape.payment.dto.request.PaymentRequest;
|
||||
import roomescape.payment.dto.response.PaymentCancelResponse;
|
||||
import roomescape.payment.dto.response.PaymentResponse;
|
||||
import roomescape.payment.dto.response.TossPaymentErrorResponse;
|
||||
|
||||
@Component
|
||||
public class TossPaymentClient {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(TossPaymentClient.class);
|
||||
|
||||
private final RestClient restClient;
|
||||
|
||||
public TossPaymentClient(RestClient.Builder restClientBuilder) {
|
||||
this.restClient = restClientBuilder.build();
|
||||
}
|
||||
|
||||
public PaymentResponse confirmPayment(PaymentRequest paymentRequest) {
|
||||
logPaymentInfo(paymentRequest);
|
||||
return restClient.post()
|
||||
.uri("/v1/payments/confirm")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(paymentRequest)
|
||||
.retrieve()
|
||||
.onStatus(status -> status.is4xxClientError() || status.is5xxServerError(),
|
||||
(req, res) -> handlePaymentError(res))
|
||||
.body(PaymentResponse.class);
|
||||
}
|
||||
|
||||
public PaymentCancelResponse cancelPayment(PaymentCancelRequest cancelRequest) {
|
||||
logPaymentCancelInfo(cancelRequest);
|
||||
Map<String, String> param = Map.of("cancelReason", cancelRequest.cancelReason());
|
||||
|
||||
return restClient.post()
|
||||
.uri("/v1/payments/{paymentKey}/cancel", cancelRequest.paymentKey())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(param)
|
||||
.retrieve()
|
||||
.onStatus(status -> status.is4xxClientError() || status.is5xxServerError(),
|
||||
(req, res) -> handlePaymentError(res))
|
||||
.body(PaymentCancelResponse.class);
|
||||
}
|
||||
|
||||
private void logPaymentInfo(PaymentRequest paymentRequest) {
|
||||
log.info("결제 승인 요청: paymentKey={}, orderId={}, amount={}, paymentType={}",
|
||||
paymentRequest.paymentKey(), paymentRequest.orderId(), paymentRequest.amount(),
|
||||
paymentRequest.paymentType());
|
||||
}
|
||||
|
||||
private void logPaymentCancelInfo(PaymentCancelRequest cancelRequest) {
|
||||
log.info("결제 취소 요청: paymentKey={}, amount={}, cancelReason={}",
|
||||
cancelRequest.paymentKey(), cancelRequest.amount(), cancelRequest.cancelReason());
|
||||
}
|
||||
|
||||
private void handlePaymentError(ClientHttpResponse res)
|
||||
throws IOException {
|
||||
HttpStatusCode statusCode = res.getStatusCode();
|
||||
ErrorType errorType = getErrorTypeByStatusCode(statusCode);
|
||||
TossPaymentErrorResponse errorResponse = getErrorResponse(res);
|
||||
|
||||
throw new RoomescapeException(errorType,
|
||||
String.format("[ErrorCode = %s, ErrorMessage = %s]", errorResponse.code(), errorResponse.message()),
|
||||
statusCode);
|
||||
}
|
||||
|
||||
private TossPaymentErrorResponse getErrorResponse(ClientHttpResponse res) throws IOException {
|
||||
InputStream body = res.getBody();
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
TossPaymentErrorResponse errorResponse = objectMapper.readValue(body, TossPaymentErrorResponse.class);
|
||||
body.close();
|
||||
return errorResponse;
|
||||
}
|
||||
|
||||
private ErrorType getErrorTypeByStatusCode(HttpStatusCode statusCode) {
|
||||
if (statusCode.is4xxClientError()) {
|
||||
return ErrorType.PAYMENT_ERROR;
|
||||
}
|
||||
return ErrorType.PAYMENT_SERVER_ERROR;
|
||||
}
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
package roomescape.payment.domain;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
|
||||
@Entity
|
||||
public class CanceledPayment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String paymentKey;
|
||||
private String cancelReason;
|
||||
private Long cancelAmount;
|
||||
private OffsetDateTime approvedAt;
|
||||
private OffsetDateTime canceledAt;
|
||||
|
||||
protected CanceledPayment() {
|
||||
}
|
||||
|
||||
public CanceledPayment(String paymentKey, String cancelReason, Long cancelAmount, OffsetDateTime approvedAt,
|
||||
OffsetDateTime canceledAt) {
|
||||
validateDate(approvedAt, canceledAt);
|
||||
this.paymentKey = paymentKey;
|
||||
this.cancelReason = cancelReason;
|
||||
this.cancelAmount = cancelAmount;
|
||||
this.approvedAt = approvedAt;
|
||||
this.canceledAt = canceledAt;
|
||||
}
|
||||
|
||||
private void validateDate(OffsetDateTime approvedAt, OffsetDateTime canceledAt) {
|
||||
if (canceledAt.isBefore(approvedAt)) {
|
||||
throw new RoomescapeException(ErrorType.CANCELED_BEFORE_PAYMENT,
|
||||
String.format("[approvedAt: %s, canceledAt: %s]", approvedAt, canceledAt),
|
||||
HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
public String getCancelReason() {
|
||||
return cancelReason;
|
||||
}
|
||||
|
||||
public Long getCancelAmount() {
|
||||
return cancelAmount;
|
||||
}
|
||||
|
||||
public OffsetDateTime getApprovedAt() {
|
||||
return approvedAt;
|
||||
}
|
||||
|
||||
public OffsetDateTime getCanceledAt() {
|
||||
return canceledAt;
|
||||
}
|
||||
|
||||
public void setCanceledAt(OffsetDateTime canceledAt) {
|
||||
this.canceledAt = canceledAt;
|
||||
}
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
package roomescape.payment.domain;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.OneToOne;
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
|
||||
@Entity
|
||||
public class Payment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String orderId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String paymentKey;
|
||||
|
||||
@Column(nullable = false)
|
||||
private Long totalAmount;
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "reservation_id", nullable = false)
|
||||
private Reservation reservation;
|
||||
|
||||
@Column(nullable = false)
|
||||
private OffsetDateTime approvedAt;
|
||||
|
||||
protected Payment() {
|
||||
}
|
||||
|
||||
public Payment(String orderId, String paymentKey, Long totalAmount, Reservation reservation,
|
||||
OffsetDateTime approvedAt) {
|
||||
validate(orderId, paymentKey, totalAmount, reservation, approvedAt);
|
||||
this.orderId = orderId;
|
||||
this.paymentKey = paymentKey;
|
||||
this.totalAmount = totalAmount;
|
||||
this.reservation = reservation;
|
||||
this.approvedAt = approvedAt;
|
||||
}
|
||||
|
||||
private void validate(String orderId, String paymentKey, Long totalAmount, Reservation reservation,
|
||||
OffsetDateTime approvedAt) {
|
||||
validateIsNullOrBlank(orderId, "orderId");
|
||||
validateIsNullOrBlank(paymentKey, "paymentKey");
|
||||
validateIsInvalidAmount(totalAmount);
|
||||
validateIsNull(reservation, "reservation");
|
||||
validateIsNull(approvedAt, "approvedAt");
|
||||
}
|
||||
|
||||
private void validateIsNullOrBlank(String input, String fieldName) {
|
||||
if (input == null || input.isBlank()) {
|
||||
throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[value : %s]", fieldName),
|
||||
HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateIsInvalidAmount(Long totalAmount) {
|
||||
if (totalAmount == null || totalAmount < 0) {
|
||||
throw new RoomescapeException(ErrorType.INVALID_REQUEST_DATA,
|
||||
String.format("[totalAmount : %d]", totalAmount), HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void validateIsNull(T value, String fieldName) {
|
||||
if (value == null) {
|
||||
throw new RoomescapeException(ErrorType.REQUEST_DATA_BLANK, String.format("[value : %s]", fieldName),
|
||||
HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getOrderId() {
|
||||
return orderId;
|
||||
}
|
||||
|
||||
public String getPaymentKey() {
|
||||
return paymentKey;
|
||||
}
|
||||
|
||||
public Long getTotalAmount() {
|
||||
return totalAmount;
|
||||
}
|
||||
|
||||
public Reservation getReservation() {
|
||||
return reservation;
|
||||
}
|
||||
|
||||
public OffsetDateTime getApprovedAt() {
|
||||
return approvedAt;
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
package roomescape.payment.domain.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import roomescape.payment.domain.CanceledPayment;
|
||||
|
||||
public interface CanceledPaymentRepository extends JpaRepository<CanceledPayment, Long> {
|
||||
|
||||
Optional<CanceledPayment> findByPaymentKey(String paymentKey);
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
package roomescape.payment.domain.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import roomescape.payment.domain.Payment;
|
||||
|
||||
public interface PaymentRepository extends JpaRepository<Payment, Long> {
|
||||
|
||||
Optional<Payment> findByReservationId(Long reservationId);
|
||||
|
||||
Optional<Payment> findByPaymentKey(String paymentKey);
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
package roomescape.payment.dto.request;
|
||||
|
||||
public record PaymentCancelRequest(String paymentKey, Long amount, String cancelReason) {
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
package roomescape.payment.dto.request;
|
||||
|
||||
public record PaymentRequest(String paymentKey, String orderId, Long amount, String paymentType) {
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
package roomescape.payment.dto.response;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
@JsonDeserialize(using = PaymentCancelResponseDeserializer.class)
|
||||
public record PaymentCancelResponse(
|
||||
String cancelStatus,
|
||||
String cancelReason,
|
||||
Long cancelAmount,
|
||||
OffsetDateTime canceledAt
|
||||
) {
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
package roomescape.payment.dto.response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
|
||||
|
||||
public class PaymentCancelResponseDeserializer extends StdDeserializer<PaymentCancelResponse> {
|
||||
|
||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern(
|
||||
"yyyy-MM-dd'T'HH:mm:ssXXX");
|
||||
|
||||
public PaymentCancelResponseDeserializer() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public PaymentCancelResponseDeserializer(Class<?> vc) {
|
||||
super(vc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentCancelResponse deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
|
||||
throws IOException {
|
||||
JsonNode cancels = (JsonNode)jsonParser.getCodec().readTree(jsonParser).get("cancels").get(0);
|
||||
return new PaymentCancelResponse(
|
||||
cancels.get("cancelStatus").asText(),
|
||||
cancels.get("cancelReason").asText(),
|
||||
cancels.get("cancelAmount").asLong(),
|
||||
OffsetDateTime.parse(cancels.get("canceledAt").asText())
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
package roomescape.payment.dto.response;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public record PaymentResponse(
|
||||
String paymentKey,
|
||||
String orderId,
|
||||
OffsetDateTime approvedAt,
|
||||
Long totalAmount
|
||||
) {
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
package roomescape.payment.dto.response;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import roomescape.payment.domain.Payment;
|
||||
import roomescape.reservation.dto.response.ReservationResponse;
|
||||
|
||||
public record ReservationPaymentResponse(Long id, String orderId, String paymentKey, Long totalAmount,
|
||||
ReservationResponse reservation, OffsetDateTime approvedAt) {
|
||||
|
||||
public static ReservationPaymentResponse from(Payment saved) {
|
||||
return new ReservationPaymentResponse(saved.getId(), saved.getOrderId(), saved.getPaymentKey(),
|
||||
saved.getTotalAmount(), ReservationResponse.from(saved.getReservation()), saved.getApprovedAt());
|
||||
}
|
||||
}
|
||||
@ -1,4 +0,0 @@
|
||||
package roomescape.payment.dto.response;
|
||||
|
||||
public record TossPaymentErrorResponse(String code, String message) {
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
package roomescape.payment.infrastructure.client
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParser
|
||||
import com.fasterxml.jackson.core.TreeNode
|
||||
import com.fasterxml.jackson.databind.DeserializationContext
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
import java.io.IOException
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class PaymentCancelResponseDeserializer(
|
||||
vc: Class<PaymentCancel.Response>? = null
|
||||
) : StdDeserializer<PaymentCancel.Response>(vc) {
|
||||
@Throws(IOException::class)
|
||||
override fun deserialize(
|
||||
jsonParser: JsonParser,
|
||||
deserializationContext: DeserializationContext?
|
||||
): PaymentCancel.Response {
|
||||
val cancels: JsonNode = jsonParser.codec.readTree<TreeNode>(jsonParser)
|
||||
.get("cancels")
|
||||
.get(0) as JsonNode
|
||||
|
||||
return PaymentCancel.Response(
|
||||
cancels.get("cancelStatus").asText(),
|
||||
cancels.get("cancelReason").asText(),
|
||||
cancels.get("cancelAmount").asLong(),
|
||||
OffsetDateTime.parse(cancels.get("canceledAt").asText())
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package roomescape.payment.infrastructure.client
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder
|
||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.client.RestClient
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(PaymentProperties::class)
|
||||
class PaymentConfig {
|
||||
|
||||
@Bean
|
||||
fun tossPaymentClientBuilder(
|
||||
paymentProperties: PaymentProperties,
|
||||
): RestClient.Builder {
|
||||
val settings: ClientHttpRequestFactorySettings = ClientHttpRequestFactorySettings.defaults().also {
|
||||
it.withReadTimeout(Duration.ofSeconds(paymentProperties.readTimeout.toLong()))
|
||||
it.withConnectTimeout(Duration.ofSeconds(paymentProperties.connectTimeout.toLong()))
|
||||
}
|
||||
val requestFactory = ClientHttpRequestFactoryBuilder.jdk().build(settings)
|
||||
|
||||
return RestClient.builder()
|
||||
.baseUrl(paymentProperties.apiBaseUrl)
|
||||
.defaultHeader("Authorization", getAuthorizations(paymentProperties.confirmSecretKey))
|
||||
.requestFactory(requestFactory)
|
||||
}
|
||||
|
||||
private fun getAuthorizations(secretKey: String): String {
|
||||
val encodedSecretKey = Base64.getEncoder()
|
||||
.encodeToString("$secretKey:".toByteArray(StandardCharsets.UTF_8))
|
||||
|
||||
return "Basic $encodedSecretKey"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package roomescape.payment.infrastructure.client
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties
|
||||
|
||||
@ConfigurationProperties(prefix = "payment")
|
||||
data class PaymentProperties(
|
||||
@JvmField val apiBaseUrl: String,
|
||||
@JvmField val confirmSecretKey: String,
|
||||
@JvmField val readTimeout: Int,
|
||||
@JvmField val connectTimeout: Int
|
||||
)
|
||||
@ -0,0 +1,113 @@
|
||||
package roomescape.payment.infrastructure.client
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import io.github.oshai.kotlinlogging.KLogger
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import org.springframework.http.HttpRequest
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.HttpStatusCode
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.client.ClientHttpResponse
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.client.RestClient
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.payment.web.PaymentApprove
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
import java.io.IOException
|
||||
import java.util.Map
|
||||
|
||||
@Component
|
||||
class TossPaymentClient(
|
||||
private val log: KLogger = KotlinLogging.logger {},
|
||||
tossPaymentClientBuilder: RestClient.Builder
|
||||
) {
|
||||
companion object {
|
||||
private const val CONFIRM_URL: String = "/v1/payments/confirm"
|
||||
private const val CANCEL_URL: String = "/v1/payments/{paymentKey}/cancel"
|
||||
}
|
||||
|
||||
private val tossPaymentClient: RestClient = tossPaymentClientBuilder.build()
|
||||
|
||||
fun confirmPayment(paymentRequest: PaymentApprove.Request): PaymentApprove.Response {
|
||||
logPaymentInfo(paymentRequest)
|
||||
|
||||
return tossPaymentClient.post()
|
||||
.uri(CONFIRM_URL)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(paymentRequest)
|
||||
.retrieve()
|
||||
.onStatus(
|
||||
{ status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError },
|
||||
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) }
|
||||
)
|
||||
.body(PaymentApprove.Response::class.java)
|
||||
?: throw RoomescapeException(ErrorType.PAYMENT_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
fun cancelPayment(cancelRequest: PaymentCancel.Request): PaymentCancel.Response {
|
||||
logPaymentCancelInfo(cancelRequest)
|
||||
val param = Map.of<String, String>("cancelReason", cancelRequest.cancelReason)
|
||||
|
||||
return tossPaymentClient.post()
|
||||
.uri(CANCEL_URL, cancelRequest.paymentKey)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(param)
|
||||
.retrieve()
|
||||
.onStatus(
|
||||
{ status: HttpStatusCode -> status.is4xxClientError || status.is5xxServerError },
|
||||
{ req: HttpRequest, res: ClientHttpResponse -> handlePaymentError(res) }
|
||||
)
|
||||
.body(PaymentCancel.Response::class.java)
|
||||
?: throw RoomescapeException(ErrorType.PAYMENT_SERVER_ERROR, HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
private fun logPaymentInfo(paymentRequest: PaymentApprove.Request) {
|
||||
log.info {
|
||||
"결제 승인 요청: paymentKey=${paymentRequest.paymentKey}, orderId=${paymentRequest.orderId}, " +
|
||||
"amount=${paymentRequest.amount}, paymentType=${paymentRequest.paymentType}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun logPaymentCancelInfo(cancelRequest: PaymentCancel.Request) {
|
||||
log.info {
|
||||
"결제 취소 요청: paymentKey=${cancelRequest.paymentKey}, amount=${cancelRequest.amount}, " +
|
||||
"cancelReason=${cancelRequest.cancelReason}"
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun handlePaymentError(
|
||||
res: ClientHttpResponse
|
||||
): Nothing {
|
||||
val statusCode = res.statusCode
|
||||
val errorType = getErrorTypeByStatusCode(statusCode)
|
||||
val errorResponse = getErrorResponse(res)
|
||||
|
||||
throw RoomescapeException(
|
||||
errorType,
|
||||
"[ErrorCode = ${errorResponse.code}, ErrorMessage = ${errorResponse.message}]",
|
||||
statusCode
|
||||
)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun getErrorResponse(
|
||||
res: ClientHttpResponse
|
||||
): TossPaymentErrorResponse {
|
||||
val body = res.body
|
||||
val objectMapper = ObjectMapper()
|
||||
val errorResponse = objectMapper.readValue(body, TossPaymentErrorResponse::class.java)
|
||||
body.close()
|
||||
return errorResponse
|
||||
}
|
||||
|
||||
private fun getErrorTypeByStatusCode(
|
||||
statusCode: HttpStatusCode
|
||||
): ErrorType {
|
||||
if (statusCode.is4xxClientError) {
|
||||
return ErrorType.PAYMENT_ERROR
|
||||
}
|
||||
return ErrorType.PAYMENT_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
package roomescape.payment.infrastructure.client
|
||||
|
||||
@JvmRecord
|
||||
data class TossPaymentErrorResponse(
|
||||
val code: String,
|
||||
val message: String
|
||||
)
|
||||
@ -0,0 +1,18 @@
|
||||
package roomescape.payment.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.*
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity
|
||||
@Table(name = "canceled_payment")
|
||||
class CanceledPaymentEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
|
||||
var paymentKey: String,
|
||||
var cancelReason: String,
|
||||
var cancelAmount: Long,
|
||||
var approvedAt: OffsetDateTime,
|
||||
var canceledAt: OffsetDateTime,
|
||||
)
|
||||
@ -0,0 +1,7 @@
|
||||
package roomescape.payment.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
||||
interface CanceledPaymentRepository : JpaRepository<CanceledPaymentEntity, Long> {
|
||||
fun findByPaymentKey(paymentKey: String): CanceledPaymentEntity?
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package roomescape.payment.infrastructure.persistence
|
||||
|
||||
import jakarta.persistence.*
|
||||
import roomescape.reservation.domain.Reservation
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
@Entity
|
||||
@Table(name = "payment")
|
||||
class PaymentEntity(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
var id: Long? = null,
|
||||
|
||||
@Column(nullable = false)
|
||||
var orderId: String,
|
||||
|
||||
@Column(nullable = false)
|
||||
var paymentKey: String,
|
||||
|
||||
@Column(nullable = false)
|
||||
var totalAmount: Long,
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "reservation_id", nullable = false)
|
||||
var reservation: Reservation,
|
||||
|
||||
@Column(nullable = false)
|
||||
var approvedAt: OffsetDateTime
|
||||
)
|
||||
@ -0,0 +1,14 @@
|
||||
package roomescape.payment.infrastructure.persistence
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
|
||||
interface PaymentRepository : JpaRepository<PaymentEntity, Long> {
|
||||
|
||||
fun existsByReservationId(reservationId: Long): Boolean
|
||||
|
||||
@Query("SELECT p.paymentKey FROM PaymentEntity p WHERE p.reservation.id = :reservationId")
|
||||
fun findPaymentKeyByReservationId(reservationId: Long): String?
|
||||
|
||||
fun findByPaymentKey(paymentKey: String): PaymentEntity?
|
||||
}
|
||||
@ -1,82 +0,0 @@
|
||||
package roomescape.payment.service;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.payment.domain.CanceledPayment;
|
||||
import roomescape.payment.domain.Payment;
|
||||
import roomescape.payment.domain.repository.CanceledPaymentRepository;
|
||||
import roomescape.payment.domain.repository.PaymentRepository;
|
||||
import roomescape.payment.dto.request.PaymentCancelRequest;
|
||||
import roomescape.payment.dto.response.PaymentCancelResponse;
|
||||
import roomescape.payment.dto.response.PaymentResponse;
|
||||
import roomescape.payment.dto.response.ReservationPaymentResponse;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class PaymentService {
|
||||
|
||||
private final PaymentRepository paymentRepository;
|
||||
private final CanceledPaymentRepository canceledPaymentRepository;
|
||||
|
||||
public PaymentService(PaymentRepository paymentRepository, CanceledPaymentRepository canceledPaymentRepository) {
|
||||
this.paymentRepository = paymentRepository;
|
||||
this.canceledPaymentRepository = canceledPaymentRepository;
|
||||
}
|
||||
|
||||
public ReservationPaymentResponse savePayment(PaymentResponse paymentResponse, Reservation reservation) {
|
||||
Payment payment = new Payment(paymentResponse.orderId(), paymentResponse.paymentKey(),
|
||||
paymentResponse.totalAmount(), reservation, paymentResponse.approvedAt());
|
||||
Payment saved = paymentRepository.save(payment);
|
||||
return ReservationPaymentResponse.from(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<Payment> findPaymentByReservationId(Long reservationId) {
|
||||
return paymentRepository.findByReservationId(reservationId);
|
||||
}
|
||||
|
||||
public void saveCanceledPayment(PaymentCancelResponse cancelInfo, OffsetDateTime approvedAt, String paymentKey) {
|
||||
canceledPaymentRepository.save(new CanceledPayment(
|
||||
paymentKey, cancelInfo.cancelReason(), cancelInfo.cancelAmount(), approvedAt, cancelInfo.canceledAt()));
|
||||
}
|
||||
|
||||
public PaymentCancelRequest cancelPaymentByAdmin(Long reservationId) {
|
||||
String paymentKey = findPaymentByReservationId(reservationId)
|
||||
.orElseThrow(() -> new RoomescapeException(ErrorType.PAYMENT_NOT_POUND,
|
||||
String.format("[reservationId: %d]", reservationId), HttpStatus.NOT_FOUND))
|
||||
.getPaymentKey();
|
||||
// 취소 시간은 현재 시간으로 일단 생성한 뒤, 결제 취소 완료 후 해당 시간으로 변경합니다.
|
||||
CanceledPayment canceled = cancelPayment(paymentKey, "고객 요청", OffsetDateTime.now());
|
||||
|
||||
return new PaymentCancelRequest(paymentKey, canceled.getCancelAmount(), canceled.getCancelReason());
|
||||
}
|
||||
|
||||
private CanceledPayment cancelPayment(String paymentKey, String cancelReason, OffsetDateTime canceledAt) {
|
||||
Payment payment = paymentRepository.findByPaymentKey(paymentKey)
|
||||
.orElseThrow(() -> throwPaymentNotFoundByPaymentKey(paymentKey));
|
||||
paymentRepository.delete(payment);
|
||||
|
||||
return canceledPaymentRepository.save(new CanceledPayment(paymentKey, cancelReason, payment.getTotalAmount(),
|
||||
payment.getApprovedAt(), canceledAt));
|
||||
}
|
||||
|
||||
public void updateCanceledTime(String paymentKey, OffsetDateTime canceledAt) {
|
||||
CanceledPayment canceledPayment = canceledPaymentRepository.findByPaymentKey(paymentKey)
|
||||
.orElseThrow(() -> throwPaymentNotFoundByPaymentKey(paymentKey));
|
||||
canceledPayment.setCanceledAt(canceledAt);
|
||||
}
|
||||
|
||||
private RoomescapeException throwPaymentNotFoundByPaymentKey(String paymentKey) {
|
||||
return new RoomescapeException(
|
||||
ErrorType.PAYMENT_NOT_POUND, String.format("[paymentKey: %s]", paymentKey),
|
||||
HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
65
src/main/java/roomescape/payment/web/PaymentDTO.kt
Normal file
65
src/main/java/roomescape/payment/web/PaymentDTO.kt
Normal file
@ -0,0 +1,65 @@
|
||||
package roomescape.payment.web
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||
import roomescape.payment.infrastructure.client.PaymentCancelResponseDeserializer
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import roomescape.reservation.dto.response.ReservationResponse
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class PaymentApprove {
|
||||
@JvmRecord
|
||||
data class Request(
|
||||
@JvmField val paymentKey: String,
|
||||
@JvmField val orderId: String,
|
||||
@JvmField val amount: Long,
|
||||
@JvmField val paymentType: String
|
||||
)
|
||||
|
||||
@JvmRecord
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class Response(
|
||||
@JvmField val paymentKey: String,
|
||||
@JvmField val orderId: String,
|
||||
@JvmField val approvedAt: OffsetDateTime,
|
||||
@JvmField val totalAmount: Long
|
||||
)
|
||||
}
|
||||
|
||||
class PaymentCancel {
|
||||
@JvmRecord
|
||||
data class Request(
|
||||
@JvmField val paymentKey: String,
|
||||
@JvmField val amount: Long,
|
||||
@JvmField val cancelReason: String
|
||||
)
|
||||
|
||||
@JvmRecord
|
||||
@JsonDeserialize(using = PaymentCancelResponseDeserializer::class)
|
||||
data class Response(
|
||||
@JvmField val cancelStatus: String,
|
||||
@JvmField val cancelReason: String,
|
||||
@JvmField val cancelAmount: Long,
|
||||
@JvmField val canceledAt: OffsetDateTime
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@JvmRecord
|
||||
data class ReservationPaymentResponse(
|
||||
val id: Long,
|
||||
val orderId: String,
|
||||
val paymentKey: String,
|
||||
val totalAmount: Long,
|
||||
val reservation: ReservationResponse,
|
||||
val approvedAt: OffsetDateTime
|
||||
)
|
||||
|
||||
fun PaymentEntity.toReservationPaymentResponse(): ReservationPaymentResponse = ReservationPaymentResponse(
|
||||
id = this.id!!,
|
||||
orderId = this.orderId,
|
||||
paymentKey = this.paymentKey,
|
||||
totalAmount = this.totalAmount,
|
||||
reservation = ReservationResponse.from(this.reservation),
|
||||
approvedAt = this.approvedAt
|
||||
)
|
||||
@ -30,11 +30,9 @@ import roomescape.auth.web.support.MemberId;
|
||||
import roomescape.common.dto.response.RoomescapeApiResponse;
|
||||
import roomescape.common.dto.response.RoomescapeErrorResponse;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.payment.client.TossPaymentClient;
|
||||
import roomescape.payment.dto.request.PaymentCancelRequest;
|
||||
import roomescape.payment.dto.request.PaymentRequest;
|
||||
import roomescape.payment.dto.response.PaymentCancelResponse;
|
||||
import roomescape.payment.dto.response.PaymentResponse;
|
||||
import roomescape.payment.infrastructure.client.TossPaymentClient;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
import roomescape.payment.web.PaymentCancel;
|
||||
import roomescape.reservation.dto.request.AdminReservationRequest;
|
||||
import roomescape.reservation.dto.request.ReservationRequest;
|
||||
import roomescape.reservation.dto.request.WaitingRequest;
|
||||
@ -120,13 +118,13 @@ public class ReservationController {
|
||||
return RoomescapeApiResponse.success();
|
||||
}
|
||||
|
||||
PaymentCancelRequest paymentCancelRequest = reservationWithPaymentService.removeReservationWithPayment(
|
||||
PaymentCancel.Request paymentCancelRequest = reservationWithPaymentService.removeReservationWithPayment(
|
||||
reservationId, memberId);
|
||||
|
||||
PaymentCancelResponse paymentCancelResponse = paymentClient.cancelPayment(paymentCancelRequest);
|
||||
PaymentCancel.Response paymentCancelResponse = paymentClient.cancelPayment(paymentCancelRequest);
|
||||
|
||||
reservationWithPaymentService.updateCanceledTime(paymentCancelRequest.paymentKey(),
|
||||
paymentCancelResponse.canceledAt());
|
||||
reservationWithPaymentService.updateCanceledTime(paymentCancelRequest.paymentKey,
|
||||
paymentCancelResponse.canceledAt);
|
||||
|
||||
return RoomescapeApiResponse.success();
|
||||
}
|
||||
@ -144,21 +142,21 @@ public class ReservationController {
|
||||
@MemberId @Parameter(hidden = true) Long memberId,
|
||||
HttpServletResponse response
|
||||
) {
|
||||
PaymentRequest paymentRequest = reservationRequest.getPaymentRequest();
|
||||
PaymentResponse paymentResponse = paymentClient.confirmPayment(paymentRequest);
|
||||
PaymentApprove.Request paymentRequest = reservationRequest.getPaymentRequest();
|
||||
PaymentApprove.Response paymentResponse = paymentClient.confirmPayment(paymentRequest);
|
||||
|
||||
try {
|
||||
ReservationResponse reservationResponse = reservationWithPaymentService.addReservationWithPayment(
|
||||
reservationRequest, paymentResponse, memberId);
|
||||
return getCreatedReservationResponse(reservationResponse, response);
|
||||
} catch (RoomescapeException e) {
|
||||
PaymentCancelRequest cancelRequest = new PaymentCancelRequest(paymentRequest.paymentKey(),
|
||||
paymentRequest.amount(), e.getMessage());
|
||||
PaymentCancel.Request cancelRequest = new PaymentCancel.Request(paymentRequest.paymentKey,
|
||||
paymentRequest.amount, e.getMessage());
|
||||
|
||||
PaymentCancelResponse paymentCancelResponse = paymentClient.cancelPayment(cancelRequest);
|
||||
PaymentCancel.Response paymentCancelResponse = paymentClient.cancelPayment(cancelRequest);
|
||||
|
||||
reservationWithPaymentService.saveCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt(),
|
||||
paymentRequest.paymentKey());
|
||||
reservationWithPaymentService.saveCanceledPayment(paymentCancelResponse, paymentResponse.approvedAt,
|
||||
paymentRequest.paymentKey);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ public interface ReservationRepository extends JpaRepository<Reservation, Long>,
|
||||
)
|
||||
FROM Reservation r
|
||||
JOIN r.theme t
|
||||
LEFT JOIN Payment p
|
||||
LEFT JOIN PaymentEntity p
|
||||
ON p.reservation = r
|
||||
WHERE r.member.id = :memberId
|
||||
""")
|
||||
|
||||
@ -6,7 +6,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import roomescape.payment.dto.request.PaymentRequest;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
|
||||
@Schema(name = "회원의 예약 저장 요청", description = "회원의 예약 요청시 사용됩니다.")
|
||||
public record ReservationRequest(
|
||||
@ -30,7 +30,7 @@ public record ReservationRequest(
|
||||
) {
|
||||
|
||||
@JsonIgnore
|
||||
public PaymentRequest getPaymentRequest() {
|
||||
return new PaymentRequest(paymentKey, orderId, amount, paymentType);
|
||||
public PaymentApprove.Request getPaymentRequest() {
|
||||
return new PaymentApprove.Request(paymentKey, orderId, amount, paymentType);
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,11 +5,10 @@ import java.time.OffsetDateTime;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import roomescape.payment.dto.request.PaymentCancelRequest;
|
||||
import roomescape.payment.dto.response.PaymentCancelResponse;
|
||||
import roomescape.payment.dto.response.PaymentResponse;
|
||||
import roomescape.payment.dto.response.ReservationPaymentResponse;
|
||||
import roomescape.payment.service.PaymentService;
|
||||
import roomescape.payment.business.PaymentService;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
import roomescape.payment.web.PaymentCancel;
|
||||
import roomescape.payment.web.ReservationPaymentResponse;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.dto.request.ReservationRequest;
|
||||
import roomescape.reservation.dto.response.ReservationResponse;
|
||||
@ -27,7 +26,8 @@ public class ReservationWithPaymentService {
|
||||
this.paymentService = paymentService;
|
||||
}
|
||||
|
||||
public ReservationResponse addReservationWithPayment(ReservationRequest request, PaymentResponse paymentInfo,
|
||||
public ReservationResponse addReservationWithPayment(ReservationRequest request,
|
||||
PaymentApprove.Response paymentInfo,
|
||||
Long memberId) {
|
||||
Reservation reservation = reservationService.addReservation(request, memberId);
|
||||
ReservationPaymentResponse reservationPaymentResponse = paymentService.savePayment(paymentInfo, reservation);
|
||||
@ -35,19 +35,19 @@ public class ReservationWithPaymentService {
|
||||
return reservationPaymentResponse.reservation();
|
||||
}
|
||||
|
||||
public void saveCanceledPayment(PaymentCancelResponse cancelInfo, OffsetDateTime approvedAt, String paymentKey) {
|
||||
public void saveCanceledPayment(PaymentCancel.Response cancelInfo, OffsetDateTime approvedAt, String paymentKey) {
|
||||
paymentService.saveCanceledPayment(cancelInfo, approvedAt, paymentKey);
|
||||
}
|
||||
|
||||
public PaymentCancelRequest removeReservationWithPayment(Long reservationId, Long memberId) {
|
||||
PaymentCancelRequest paymentCancelRequest = paymentService.cancelPaymentByAdmin(reservationId);
|
||||
public PaymentCancel.Request removeReservationWithPayment(Long reservationId, Long memberId) {
|
||||
PaymentCancel.Request paymentCancelRequest = paymentService.cancelPaymentByAdmin(reservationId);
|
||||
reservationService.removeReservationById(reservationId, memberId);
|
||||
return paymentCancelRequest;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public boolean isNotPaidReservation(Long reservationId) {
|
||||
return paymentService.findPaymentByReservationId(reservationId).isEmpty();
|
||||
return !paymentService.isReservationPaid(reservationId);
|
||||
}
|
||||
|
||||
public void updateCanceledTime(String paymentKey, OffsetDateTime canceledAt) {
|
||||
|
||||
@ -25,6 +25,7 @@ security:
|
||||
expire-length: 1800000 # 30 분
|
||||
|
||||
payment:
|
||||
api-base-url: https://api.tosspayments.com
|
||||
confirm-secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6
|
||||
read-timeout: 3
|
||||
connect-timeout: 30
|
||||
|
||||
@ -11,6 +11,7 @@ import roomescape.member.web.MemberController
|
||||
import roomescape.member.web.MembersResponse
|
||||
import roomescape.util.MemberFixture
|
||||
import roomescape.util.RoomescapeApiTest
|
||||
import kotlin.random.Random
|
||||
|
||||
@WebMvcTest(controllers = [MemberController::class])
|
||||
class MemberControllerTest(
|
||||
@ -22,9 +23,9 @@ class MemberControllerTest(
|
||||
val endpoint = "/members"
|
||||
|
||||
every { memberRepository.findAll() } returns listOf(
|
||||
MemberFixture.create(name = "name1"),
|
||||
MemberFixture.create(name = "name2"),
|
||||
MemberFixture.create(name = "name3"),
|
||||
MemberFixture.create(id = Random.nextLong(), name = "name1"),
|
||||
MemberFixture.create(id = Random.nextLong(), name = "name2"),
|
||||
MemberFixture.create(id = Random.nextLong(), name = "name3"),
|
||||
)
|
||||
|
||||
`when`("관리자가 보내면") {
|
||||
|
||||
@ -1,44 +1,73 @@
|
||||
package roomescape.payment.client;
|
||||
package roomescape.payment
|
||||
|
||||
import roomescape.payment.dto.request.PaymentCancelRequest;
|
||||
import roomescape.payment.dto.request.PaymentRequest;
|
||||
import roomescape.payment.SampleTossPaymentConst.amount
|
||||
import roomescape.payment.web.PaymentApprove
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
import kotlin.math.roundToLong
|
||||
|
||||
public class SampleTossPaymentConst {
|
||||
object SampleTossPaymentConst {
|
||||
@JvmField
|
||||
val paymentKey: String = "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1"
|
||||
|
||||
public static final PaymentRequest paymentRequest = new PaymentRequest(
|
||||
"5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1", "MC4wODU4ODQwMzg4NDk0", 1000L, "카드");
|
||||
@JvmField
|
||||
val orderId: String = "MC4wODU4ODQwMzg4NDk0"
|
||||
|
||||
public static final String paymentRequestJson = """
|
||||
@JvmField
|
||||
val amount: Long = 1000L
|
||||
|
||||
@JvmField
|
||||
val paymentType: String = "카드"
|
||||
|
||||
@JvmField
|
||||
val cancelReason: String = "테스트 결제 취소"
|
||||
|
||||
@JvmField
|
||||
val paymentRequest: PaymentApprove.Request = PaymentApprove.Request(
|
||||
paymentKey,
|
||||
orderId,
|
||||
amount,
|
||||
paymentType
|
||||
)
|
||||
|
||||
@JvmField
|
||||
val paymentRequestJson: String = """
|
||||
{
|
||||
"paymentKey": "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1",
|
||||
"orderId": "MC4wODU4ODQwMzg4NDk0",
|
||||
"amount": 1000,
|
||||
"paymentType": "카드"
|
||||
"paymentKey": "$paymentKey",
|
||||
"orderId": "$orderId",
|
||||
"amount": $amount,
|
||||
"paymentType": "$paymentType"
|
||||
}
|
||||
""";
|
||||
""".trimIndent()
|
||||
|
||||
public static final PaymentCancelRequest cancelRequest = new PaymentCancelRequest(
|
||||
"5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1", 1000L, "테스트 결제 취소");
|
||||
@JvmField
|
||||
val cancelRequest: PaymentCancel.Request = PaymentCancel.Request(
|
||||
paymentKey,
|
||||
amount,
|
||||
cancelReason
|
||||
)
|
||||
|
||||
public static final String cancelRequestJson = """
|
||||
@JvmField
|
||||
val cancelRequestJson: String = """
|
||||
{
|
||||
"cancelReason": "테스트 결제 취소"
|
||||
"cancelReason": "$cancelReason"
|
||||
}
|
||||
""";
|
||||
""".trimIndent()
|
||||
|
||||
public static final String tossPaymentErrorJson = """
|
||||
@JvmField
|
||||
val tossPaymentErrorJson: String = """
|
||||
{
|
||||
"code": "ERROR_CODE",
|
||||
"message": "Error message"
|
||||
}
|
||||
""";
|
||||
""".trimIndent()
|
||||
|
||||
public static final String confirmJson = """
|
||||
@JvmField
|
||||
val confirmJson: String = """
|
||||
{
|
||||
"mId": "tosspayments",
|
||||
"lastTransactionKey": "9C62B18EEF0DE3EB7F4422EB6D14BC6E",
|
||||
"paymentKey": "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1",
|
||||
"orderId": "MC4wODU4ODQwMzg4NDk0",
|
||||
"paymentKey": "$paymentKey",
|
||||
"orderId": "$orderId",
|
||||
"orderName": "토스 티셔츠 외 2건",
|
||||
"taxExemptionAmount": 0,
|
||||
"status": "DONE",
|
||||
@ -88,22 +117,23 @@ public class SampleTossPaymentConst {
|
||||
"url": "https://api.tosspayments.com/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/checkout"
|
||||
},
|
||||
"currency": "KRW",
|
||||
"totalAmount": 1000,
|
||||
"balanceAmount": 1000,
|
||||
"suppliedAmount": 909,
|
||||
"vat": 91,
|
||||
"totalAmount": $amount,
|
||||
"balanceAmount": $amount,
|
||||
"suppliedAmount": ${(amount / 1.1).roundToLong()},
|
||||
"vat": ${amount - (amount / 1.1).roundToLong()},
|
||||
"taxFreeAmount": 0,
|
||||
"method": "카드",
|
||||
"method": "$paymentType",
|
||||
"version": "2022-11-16"
|
||||
}
|
||||
""";
|
||||
""".trimIndent()
|
||||
|
||||
public static final String cancelJson = """
|
||||
@JvmField
|
||||
val cancelJson: String = """
|
||||
{
|
||||
"mId": "tosspayments",
|
||||
"lastTransactionKey": "090A796806E726BBB929F4A2CA7DB9A7",
|
||||
"paymentKey": "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1",
|
||||
"orderId": "MC4wODU4ODQwMzg4NDk0",
|
||||
"paymentKey": "$paymentKey",
|
||||
"orderId": "$orderId",
|
||||
"orderName": "토스 티셔츠 외 2건",
|
||||
"taxExemptionAmount": 0,
|
||||
"status": "CANCELED",
|
||||
@ -135,12 +165,12 @@ public class SampleTossPaymentConst {
|
||||
"cancels": [
|
||||
{
|
||||
"transactionKey": "090A796806E726BBB929F4A2CA7DB9A7",
|
||||
"cancelReason": "테스트 결제 취소",
|
||||
"cancelReason": "$cancelReason",
|
||||
"taxExemptionAmount": 0,
|
||||
"canceledAt": "2024-02-13T12:20:23+09:00",
|
||||
"easyPayDiscountAmount": 0,
|
||||
"receiptKey": null,
|
||||
"cancelAmount": 1000,
|
||||
"cancelAmount": $amount,
|
||||
"taxFreeAmount": 0,
|
||||
"refundableAmount": 0,
|
||||
"cancelStatus": "DONE",
|
||||
@ -166,13 +196,17 @@ public class SampleTossPaymentConst {
|
||||
"url": "https://api.tosspayments.com/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/checkout"
|
||||
},
|
||||
"currency": "KRW",
|
||||
"totalAmount": 1000,
|
||||
"totalAmount": $amount,
|
||||
"balanceAmount": 0,
|
||||
"suppliedAmount": 0,
|
||||
"vat": 0,
|
||||
"taxFreeAmount": 0,
|
||||
"method": "카드",
|
||||
"method": "$paymentType",
|
||||
"version": "2022-11-16"
|
||||
}
|
||||
""";
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println((amount / 1.1).roundToLong())
|
||||
}
|
||||
129
src/test/java/roomescape/payment/business/PaymentServiceKTest.kt
Normal file
129
src/test/java/roomescape/payment/business/PaymentServiceKTest.kt
Normal file
@ -0,0 +1,129 @@
|
||||
package roomescape.payment.business
|
||||
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import org.springframework.http.HttpStatus
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository
|
||||
import roomescape.payment.infrastructure.persistence.PaymentRepository
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
import roomescape.util.PaymentFixture
|
||||
import java.time.OffsetDateTime
|
||||
|
||||
class PaymentServiceKTest : FunSpec({
|
||||
val paymentRepository: PaymentRepository = mockk()
|
||||
val canceledPaymentRepository: CanceledPaymentRepository = mockk()
|
||||
|
||||
val paymentService = PaymentService(paymentRepository, canceledPaymentRepository)
|
||||
|
||||
context("cancelPaymentByAdmin") {
|
||||
val reservationId = 1L
|
||||
test("reservationId로 paymentKey를 찾을 수 없으면 예외를 던진다.") {
|
||||
every { paymentRepository.findPaymentKeyByReservationId(reservationId) } returns null
|
||||
|
||||
val exception = shouldThrow<RoomescapeException> {
|
||||
paymentService.cancelPaymentByAdmin(reservationId)
|
||||
}
|
||||
|
||||
assertSoftly(exception) {
|
||||
this.errorType shouldBe ErrorType.PAYMENT_NOT_FOUND
|
||||
this.httpStatus shouldBe HttpStatus.NOT_FOUND
|
||||
}
|
||||
}
|
||||
|
||||
context("reservationId로 paymentKey를 찾고난 후") {
|
||||
val paymentKey = "test-payment-key"
|
||||
|
||||
every {
|
||||
paymentRepository.findPaymentKeyByReservationId(reservationId)
|
||||
} returns paymentKey
|
||||
|
||||
test("해당 paymentKey로 paymentEntity를 찾을 수 없으면 예외를 던진다.") {
|
||||
every {
|
||||
paymentRepository.findByPaymentKey(paymentKey)
|
||||
} returns null
|
||||
|
||||
val exception = shouldThrow<RoomescapeException> {
|
||||
paymentService.cancelPaymentByAdmin(reservationId)
|
||||
}
|
||||
|
||||
assertSoftly(exception) {
|
||||
this.errorType shouldBe ErrorType.PAYMENT_NOT_FOUND
|
||||
this.httpStatus shouldBe HttpStatus.NOT_FOUND
|
||||
}
|
||||
}
|
||||
|
||||
test("해당 paymentKey로 paymentEntity를 찾고, cancelPaymentEntity를 저장한다.") {
|
||||
val paymentEntity = PaymentFixture.create(paymentKey = paymentKey)
|
||||
|
||||
every {
|
||||
paymentRepository.findByPaymentKey(paymentKey)
|
||||
} returns paymentEntity.also {
|
||||
every {
|
||||
paymentRepository.delete(it)
|
||||
} just runs
|
||||
}
|
||||
|
||||
every {
|
||||
canceledPaymentRepository.save(any())
|
||||
} returns PaymentFixture.createCanceled(
|
||||
id = 1L,
|
||||
paymentKey = paymentKey,
|
||||
cancelAmount = paymentEntity.totalAmount,
|
||||
)
|
||||
|
||||
val result: PaymentCancel.Request = paymentService.cancelPaymentByAdmin(reservationId)
|
||||
|
||||
assertSoftly(result) {
|
||||
this.paymentKey shouldBe paymentKey
|
||||
this.amount shouldBe paymentEntity.totalAmount
|
||||
this.cancelReason shouldBe "고객 요청"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context("updateCanceledTime") {
|
||||
val paymentKey = "test-payment-key"
|
||||
val canceledAt = OffsetDateTime.now()
|
||||
|
||||
test("paymentKey로 canceledPaymentEntity를 찾을 수 없으면 예외를 던진다.") {
|
||||
every {
|
||||
canceledPaymentRepository.findByPaymentKey(paymentKey)
|
||||
} returns null
|
||||
|
||||
val exception = shouldThrow<RoomescapeException> {
|
||||
paymentService.updateCanceledTime(paymentKey, canceledAt)
|
||||
}
|
||||
|
||||
assertSoftly(exception) {
|
||||
this.errorType shouldBe ErrorType.PAYMENT_NOT_FOUND
|
||||
this.httpStatus shouldBe HttpStatus.NOT_FOUND
|
||||
}
|
||||
}
|
||||
|
||||
test("paymentKey로 canceledPaymentEntity를 찾고, canceledAt을 업데이트한다.") {
|
||||
val canceledPaymentEntity = PaymentFixture.createCanceled(
|
||||
paymentKey = paymentKey,
|
||||
canceledAt = canceledAt.minusMinutes(1)
|
||||
)
|
||||
|
||||
every {
|
||||
canceledPaymentRepository.findByPaymentKey(paymentKey)
|
||||
} returns canceledPaymentEntity
|
||||
|
||||
paymentService.updateCanceledTime(paymentKey, canceledAt)
|
||||
|
||||
assertSoftly(canceledPaymentEntity) {
|
||||
this.canceledAt shouldBe canceledAt
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -1,4 +1,4 @@
|
||||
package roomescape.payment.service;
|
||||
package roomescape.payment.business;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
@ -13,19 +13,20 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
|
||||
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.payment.domain.repository.CanceledPaymentRepository;
|
||||
import roomescape.payment.dto.request.PaymentCancelRequest;
|
||||
import roomescape.payment.dto.response.PaymentResponse;
|
||||
import roomescape.payment.dto.response.ReservationPaymentResponse;
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity;
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
import roomescape.payment.web.PaymentCancel;
|
||||
import roomescape.payment.web.ReservationPaymentResponse;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.reservation.domain.repository.ReservationRepository;
|
||||
import roomescape.reservation.domain.repository.ReservationTimeRepository;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.theme.domain.Theme;
|
||||
import roomescape.theme.domain.repository.ThemeRepository;
|
||||
|
||||
@ -50,7 +51,8 @@ class PaymentServiceTest {
|
||||
@DisplayName("결제 정보를 저장한다.")
|
||||
void savePayment() {
|
||||
// given
|
||||
PaymentResponse paymentInfo = new PaymentResponse("payment-key", "order-id", OffsetDateTime.now(), 10000L);
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
@ -64,14 +66,15 @@ class PaymentServiceTest {
|
||||
|
||||
// then
|
||||
assertThat(reservationPaymentResponse.reservation().id()).isEqualTo(reservation.getId());
|
||||
assertThat(reservationPaymentResponse.paymentKey()).isEqualTo(paymentInfo.paymentKey());
|
||||
assertThat(reservationPaymentResponse.paymentKey()).isEqualTo(paymentInfo.paymentKey);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약 ID로 결제 정보를 제거하고, 결제 취소 테이블에 취소 정보를 저장한다.")
|
||||
void cancelPaymentByAdmin() {
|
||||
// given
|
||||
PaymentResponse paymentInfo = new PaymentResponse("payment-key", "order-id", OffsetDateTime.now(), 10000L);
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
@ -83,13 +86,13 @@ class PaymentServiceTest {
|
||||
paymentService.savePayment(paymentInfo, reservation);
|
||||
|
||||
// when
|
||||
PaymentCancelRequest paymentCancelRequest = paymentService.cancelPaymentByAdmin(reservation.getId());
|
||||
PaymentCancel.Request paymentCancelRequest = paymentService.cancelPaymentByAdmin(reservation.getId());
|
||||
|
||||
// then
|
||||
assertThat(canceledPaymentRepository.findByPaymentKey("payment-key")).isNotEmpty();
|
||||
assertThat(paymentCancelRequest.paymentKey()).isEqualTo(paymentInfo.paymentKey());
|
||||
assertThat(paymentCancelRequest.cancelReason()).isEqualTo("고객 요청");
|
||||
assertThat(paymentCancelRequest.amount()).isEqualTo(10000L);
|
||||
assertThat(canceledPaymentRepository.findByPaymentKey("payment-key")).isNotNull();
|
||||
assertThat(paymentCancelRequest.paymentKey).isEqualTo(paymentInfo.paymentKey);
|
||||
assertThat(paymentCancelRequest.cancelReason).isEqualTo("고객 요청");
|
||||
assertThat(paymentCancelRequest.amount).isEqualTo(10000L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -107,7 +110,8 @@ class PaymentServiceTest {
|
||||
@DisplayName("결제 취소 정보에 있는 취소 시간을 업데이트한다.")
|
||||
void updateCanceledTime() {
|
||||
// given
|
||||
PaymentResponse paymentInfo = new PaymentResponse("payment-key", "order-id", OffsetDateTime.now(), 10000L);
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
@ -121,11 +125,13 @@ class PaymentServiceTest {
|
||||
|
||||
// when
|
||||
OffsetDateTime canceledAt = OffsetDateTime.now().plusHours(2L);
|
||||
paymentService.updateCanceledTime(paymentInfo.paymentKey(), canceledAt);
|
||||
paymentService.updateCanceledTime(paymentInfo.paymentKey, canceledAt);
|
||||
|
||||
// then
|
||||
canceledPaymentRepository.findByPaymentKey(paymentInfo.paymentKey())
|
||||
.ifPresent(canceledPayment -> assertThat(canceledPayment.getCanceledAt()).isEqualTo(canceledAt));
|
||||
CanceledPaymentEntity canceledPayment = canceledPaymentRepository.findByPaymentKey(paymentInfo.paymentKey);
|
||||
|
||||
assertThat(canceledPayment).isNotNull();
|
||||
assertThat(canceledPayment.getCanceledAt()).isEqualTo(canceledAt);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -1,116 +0,0 @@
|
||||
package roomescape.payment.client;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
|
||||
import static org.springframework.test.web.client.match.MockRestRequestMatchers.*;
|
||||
import static org.springframework.test.web.client.response.MockRestResponseCreators.*;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.client.MockRestServiceServer;
|
||||
|
||||
import roomescape.payment.dto.request.PaymentCancelRequest;
|
||||
import roomescape.payment.dto.request.PaymentRequest;
|
||||
import roomescape.payment.dto.response.PaymentCancelResponse;
|
||||
import roomescape.payment.dto.response.PaymentResponse;
|
||||
import roomescape.common.exception.ErrorType;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
|
||||
@RestClientTest(TossPaymentClient.class)
|
||||
class TossPaymentClientTest {
|
||||
|
||||
@Autowired
|
||||
private TossPaymentClient tossPaymentClient;
|
||||
|
||||
@Autowired
|
||||
private MockRestServiceServer mockServer;
|
||||
|
||||
@Test
|
||||
@DisplayName("결제를 승인한다.")
|
||||
void confirmPayment() {
|
||||
// given
|
||||
mockServer.expect(requestTo("/v1/payments/confirm"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(content().json(SampleTossPaymentConst.paymentRequestJson))
|
||||
.andRespond(withStatus(HttpStatus.OK)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.confirmJson));
|
||||
|
||||
// when
|
||||
PaymentRequest paymentRequest = SampleTossPaymentConst.paymentRequest;
|
||||
PaymentResponse paymentResponse = tossPaymentClient.confirmPayment(paymentRequest);
|
||||
|
||||
// then
|
||||
assertThat(paymentResponse.paymentKey()).isEqualTo(paymentRequest.paymentKey());
|
||||
assertThat(paymentResponse.orderId()).isEqualTo(paymentRequest.orderId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("결제를 취소한다.")
|
||||
void cancelPayment() {
|
||||
// given
|
||||
mockServer.expect(requestTo("/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/cancel"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(content().json(SampleTossPaymentConst.cancelRequestJson))
|
||||
.andRespond(withStatus(HttpStatus.OK)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.cancelJson));
|
||||
|
||||
// when
|
||||
PaymentCancelRequest cancelRequest = SampleTossPaymentConst.cancelRequest;
|
||||
PaymentCancelResponse paymentCancelResponse = tossPaymentClient.cancelPayment(cancelRequest);
|
||||
|
||||
// then
|
||||
assertThat(paymentCancelResponse.cancelStatus()).isEqualTo("DONE");
|
||||
assertThat(paymentCancelResponse.cancelReason()).isEqualTo(cancelRequest.cancelReason());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("결제 승인 중 400 에러가 발생한다.")
|
||||
void confirmPaymentWithError() {
|
||||
// given
|
||||
mockServer.expect(requestTo("/v1/payments/confirm"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(content().json(SampleTossPaymentConst.paymentRequestJson))
|
||||
.andRespond(withStatus(HttpStatus.BAD_REQUEST)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.tossPaymentErrorJson));
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() -> tossPaymentClient.confirmPayment(SampleTossPaymentConst.paymentRequest))
|
||||
.isInstanceOf(RoomescapeException.class)
|
||||
.hasFieldOrPropertyWithValue("errorType", ErrorType.PAYMENT_ERROR)
|
||||
.hasFieldOrPropertyWithValue("invalidValue",
|
||||
"[ErrorCode = ERROR_CODE, ErrorMessage = Error message]")
|
||||
.hasFieldOrPropertyWithValue("httpStatus", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("결제 취소 중 500 에러가 발생한다.")
|
||||
void cancelPaymentWithError() {
|
||||
// given
|
||||
mockServer.expect(requestTo("/v1/payments/5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1/cancel"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
|
||||
.andExpect(content().json(SampleTossPaymentConst.cancelRequestJson))
|
||||
.andRespond(withStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.tossPaymentErrorJson));
|
||||
|
||||
// when & then
|
||||
assertThatThrownBy(() -> tossPaymentClient.cancelPayment(SampleTossPaymentConst.cancelRequest))
|
||||
.isInstanceOf(RoomescapeException.class)
|
||||
.hasFieldOrPropertyWithValue("errorType", ErrorType.PAYMENT_SERVER_ERROR)
|
||||
.hasFieldOrPropertyWithValue("invalidValue",
|
||||
"[ErrorCode = ERROR_CODE, ErrorMessage = Error message]")
|
||||
.hasFieldOrPropertyWithValue("httpStatus", HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
package roomescape.payment.domain;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
|
||||
class CanceledPaymentTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("취소 날짜가 승인 날짜 이전이면 예외가 발생한다")
|
||||
void invalidDate() {
|
||||
OffsetDateTime approvedAt = OffsetDateTime.now();
|
||||
OffsetDateTime canceledAt = approvedAt.minusMinutes(1L);
|
||||
assertThatThrownBy(() -> new CanceledPayment("payment-key", "reason", 10000L, approvedAt, canceledAt))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
}
|
||||
@ -1,78 +0,0 @@
|
||||
package roomescape.payment.domain;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.junit.jupiter.params.provider.NullAndEmptySource;
|
||||
import org.junit.jupiter.params.provider.NullSource;
|
||||
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
import roomescape.common.exception.RoomescapeException;
|
||||
import roomescape.theme.domain.Theme;
|
||||
|
||||
class PaymentTest {
|
||||
|
||||
private Reservation reservation;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
LocalDate now = LocalDate.now();
|
||||
ReservationTime reservationTime = new ReservationTime(LocalTime.now());
|
||||
Theme theme = new Theme("name", "desc", "thumb");
|
||||
Member member = new Member(null, "name", "email", "password", Role.MEMBER);
|
||||
|
||||
reservation = new Reservation(now, reservationTime, theme, member, ReservationStatus.CONFIRMED);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@DisplayName("paymentKey가 null 또는 빈값이면 예외가 발생한다.")
|
||||
@NullAndEmptySource
|
||||
void invalidPaymentKey(String paymentKey) {
|
||||
assertThatThrownBy(() -> new Payment("order-id", paymentKey, 10000L, reservation, OffsetDateTime.now()))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@DisplayName("orderId가 null 또는 빈값이면 예외가 발생한다.")
|
||||
@NullAndEmptySource
|
||||
void invalidOrderId(String orderId) {
|
||||
assertThatThrownBy(() -> new Payment(orderId, "payment-key", 10000L, reservation, OffsetDateTime.now()))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@DisplayName("amount가 null 또는 0 이하면 예외가 발생한다.")
|
||||
@CsvSource(value = {"null", "-1"}, nullValues = {"null"})
|
||||
void invalidOrderId(Long totalAmount) {
|
||||
assertThatThrownBy(
|
||||
() -> new Payment("orderId", "payment-key", totalAmount, reservation, OffsetDateTime.now()))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@DisplayName("Reservation이 null이면 예외가 발생한다.")
|
||||
@NullSource
|
||||
void invalidReservation(Reservation reservation) {
|
||||
assertThatThrownBy(() -> new Payment("orderId", "payment-key", 10000L, reservation, OffsetDateTime.now()))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@DisplayName("승인 날짜가 null이면 예외가 발생한다.")
|
||||
@NullSource
|
||||
void invalidApprovedAt(OffsetDateTime approvedAt) {
|
||||
assertThatThrownBy(() -> new Payment("orderId", "payment-key", 10000L, reservation, approvedAt))
|
||||
.isInstanceOf(RoomescapeException.class);
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
package roomescape.payment.dto.response;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
|
||||
class PaymentCancelResponseDeserializerTest {
|
||||
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper();
|
||||
SimpleModule simpleModule = new SimpleModule();
|
||||
simpleModule.addDeserializer(PaymentCancelResponse.class, new PaymentCancelResponseDeserializer());
|
||||
objectMapper.registerModule(simpleModule);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("결제 취소 정보를 역직렬화하여 PaymentCancelResponse 객체를 생성한다.")
|
||||
void deserialize() {
|
||||
// given
|
||||
String json = """
|
||||
{
|
||||
"notUsedField": "notUsedValue",
|
||||
"cancels": [
|
||||
{
|
||||
"cancelStatus": "CANCELLED",
|
||||
"cancelReason": "테스트 결제 취소",
|
||||
"cancelAmount": 10000,
|
||||
"canceledAt": "2021-07-01T10:10:10+09:00",
|
||||
"notUsedField": "notUsedValue"
|
||||
}
|
||||
]
|
||||
}""";
|
||||
|
||||
// when
|
||||
PaymentCancelResponse response = assertDoesNotThrow(
|
||||
() -> objectMapper.readValue(json, PaymentCancelResponse.class));
|
||||
|
||||
// then
|
||||
assertEquals("CANCELLED", response.cancelStatus());
|
||||
assertEquals("테스트 결제 취소", response.cancelReason());
|
||||
assertEquals(10000, response.cancelAmount());
|
||||
assertEquals("2021-07-01T10:10:10+09:00", response.canceledAt().toString());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,139 @@
|
||||
package roomescape.payment.infrastructure.client
|
||||
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest
|
||||
import org.springframework.http.HttpMethod
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.web.client.MockRestServiceServer
|
||||
import org.springframework.test.web.client.ResponseActions
|
||||
import org.springframework.test.web.client.match.MockRestRequestMatchers.*
|
||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withStatus
|
||||
import org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess
|
||||
import roomescape.common.exception.ErrorType
|
||||
import roomescape.common.exception.RoomescapeException
|
||||
import roomescape.payment.SampleTossPaymentConst
|
||||
import roomescape.payment.web.PaymentApprove
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
|
||||
@RestClientTest(TossPaymentClient::class)
|
||||
class TossPaymentClientTest(
|
||||
@Autowired val client: TossPaymentClient,
|
||||
@Autowired val mockServer: MockRestServiceServer
|
||||
) : FunSpec() {
|
||||
|
||||
init {
|
||||
context("결제 승인 요청") {
|
||||
fun commonAction(): ResponseActions = mockServer.expect {
|
||||
requestTo("/v1/payments/confirm")
|
||||
}.andExpect {
|
||||
method(HttpMethod.POST)
|
||||
}.andExpect {
|
||||
content().contentType(MediaType.APPLICATION_JSON)
|
||||
}.andExpect {
|
||||
content().json(SampleTossPaymentConst.paymentRequestJson)
|
||||
}
|
||||
|
||||
test("성공 응답") {
|
||||
commonAction().andRespond {
|
||||
withSuccess()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.confirmJson)
|
||||
.createResponse(it)
|
||||
}
|
||||
|
||||
// when
|
||||
val paymentRequest = SampleTossPaymentConst.paymentRequest
|
||||
val paymentResponse: PaymentApprove.Response = client.confirmPayment(paymentRequest)
|
||||
|
||||
assertSoftly(paymentResponse) {
|
||||
this.paymentKey shouldBe paymentRequest.paymentKey
|
||||
this.orderId shouldBe paymentRequest.orderId
|
||||
this.totalAmount shouldBe paymentRequest.amount
|
||||
}
|
||||
}
|
||||
|
||||
test("400 에러 발생") {
|
||||
commonAction().andRespond {
|
||||
withStatus(HttpStatus.BAD_REQUEST)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.tossPaymentErrorJson)
|
||||
.createResponse(it)
|
||||
}
|
||||
|
||||
// when
|
||||
val paymentRequest = SampleTossPaymentConst.paymentRequest
|
||||
|
||||
// then
|
||||
val exception = shouldThrow<RoomescapeException> {
|
||||
client.confirmPayment(paymentRequest)
|
||||
}
|
||||
|
||||
assertSoftly(exception) {
|
||||
this.errorType shouldBe ErrorType.PAYMENT_ERROR
|
||||
this.invalidValue shouldBe "[ErrorCode = ERROR_CODE, ErrorMessage = Error message]"
|
||||
this.httpStatus shouldBe HttpStatus.BAD_REQUEST
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
context("결제 취소 요청") {
|
||||
fun commonAction(): ResponseActions = mockServer.expect {
|
||||
requestTo("/v1/payments/${SampleTossPaymentConst.paymentKey}/cancel")
|
||||
}.andExpect {
|
||||
method(HttpMethod.POST)
|
||||
}.andExpect {
|
||||
content().contentType(MediaType.APPLICATION_JSON)
|
||||
}.andExpect {
|
||||
content().json(SampleTossPaymentConst.cancelRequestJson)
|
||||
}
|
||||
|
||||
test("성공 응답") {
|
||||
commonAction().andRespond {
|
||||
withSuccess()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.cancelJson)
|
||||
.createResponse(it)
|
||||
}
|
||||
|
||||
// when
|
||||
val cancelRequest: PaymentCancel.Request = SampleTossPaymentConst.cancelRequest
|
||||
val cancelResponse: PaymentCancel.Response = client.cancelPayment(cancelRequest)
|
||||
|
||||
assertSoftly(cancelResponse) {
|
||||
this.cancelStatus shouldBe "DONE"
|
||||
this.cancelReason shouldBe cancelRequest.cancelReason
|
||||
this.cancelAmount shouldBe cancelRequest.amount
|
||||
}
|
||||
}
|
||||
|
||||
test("500 에러 발생") {
|
||||
commonAction().andRespond {
|
||||
withStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body(SampleTossPaymentConst.tossPaymentErrorJson)
|
||||
.createResponse(it)
|
||||
}
|
||||
|
||||
// when
|
||||
val cancelRequest: PaymentCancel.Request = SampleTossPaymentConst.cancelRequest
|
||||
|
||||
// then
|
||||
val exception = shouldThrow<RoomescapeException> {
|
||||
client.cancelPayment(cancelRequest)
|
||||
}
|
||||
|
||||
assertSoftly(exception) {
|
||||
this.errorType shouldBe ErrorType.PAYMENT_SERVER_ERROR
|
||||
this.invalidValue shouldBe "[ErrorCode = ERROR_CODE, ErrorMessage = Error message]"
|
||||
this.httpStatus shouldBe HttpStatus.INTERNAL_SERVER_ERROR
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
package roomescape.payment.infrastructure.persistence
|
||||
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import roomescape.util.PaymentFixture
|
||||
import java.util.*
|
||||
|
||||
@DataJpaTest
|
||||
class CanceledPaymentRepositoryTest(
|
||||
@Autowired val canceledPaymentRepository: CanceledPaymentRepository,
|
||||
): FunSpec() {
|
||||
init {
|
||||
context("paymentKey로 CanceledPaymentEntity 조회") {
|
||||
val paymentKey = "test-payment-key"
|
||||
beforeTest {
|
||||
PaymentFixture.createCanceled(paymentKey = paymentKey)
|
||||
.also { canceledPaymentRepository.save(it) }
|
||||
}
|
||||
|
||||
test("정상 반환") {
|
||||
canceledPaymentRepository.findByPaymentKey(paymentKey)?.let {
|
||||
assertSoftly(it) {
|
||||
this.paymentKey shouldBe paymentKey
|
||||
}
|
||||
} ?: throw AssertionError("Unexpected null value")
|
||||
}
|
||||
|
||||
test("null 반환") {
|
||||
canceledPaymentRepository.findByPaymentKey(UUID.randomUUID().toString())
|
||||
.also { it shouldBe null }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
package roomescape.payment.infrastructure.persistence
|
||||
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import jakarta.persistence.EntityManager
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import roomescape.util.PaymentFixture
|
||||
import roomescape.util.ReservationFixture
|
||||
|
||||
@DataJpaTest
|
||||
class PaymentRepositoryTest(
|
||||
@Autowired val paymentRepository: PaymentRepository,
|
||||
@Autowired val entityManager: EntityManager
|
||||
) : FunSpec() {
|
||||
|
||||
var reservationId: Long = 0L
|
||||
|
||||
init {
|
||||
context("existsByReservationId") {
|
||||
beforeTest {
|
||||
reservationId = setupReservation()
|
||||
PaymentFixture.create(reservationId = reservationId)
|
||||
.also { paymentRepository.save(it) }
|
||||
}
|
||||
|
||||
test("true") {
|
||||
paymentRepository.existsByReservationId(reservationId)
|
||||
.also { it shouldBe true }
|
||||
}
|
||||
|
||||
test("false") {
|
||||
paymentRepository.existsByReservationId(reservationId + 1)
|
||||
.also { it shouldBe false }
|
||||
}
|
||||
}
|
||||
|
||||
context("findPaymentKeyByReservationId") {
|
||||
lateinit var paymentKey: String
|
||||
|
||||
beforeTest {
|
||||
reservationId = setupReservation()
|
||||
paymentKey = PaymentFixture.create(reservationId = reservationId)
|
||||
.also { paymentRepository.save(it) }
|
||||
.paymentKey
|
||||
}
|
||||
|
||||
test("정상 반환") {
|
||||
paymentRepository.findPaymentKeyByReservationId(reservationId)
|
||||
?.let { it shouldBe paymentKey }
|
||||
?: throw AssertionError("Unexpected null value")
|
||||
}
|
||||
|
||||
test("null 반환") {
|
||||
paymentRepository.findPaymentKeyByReservationId(reservationId + 1)
|
||||
.also { it shouldBe null }
|
||||
}
|
||||
}
|
||||
|
||||
context("findByPaymentKey") {
|
||||
lateinit var payment: PaymentEntity
|
||||
|
||||
beforeTest {
|
||||
reservationId = setupReservation()
|
||||
payment = PaymentFixture.create(reservationId = reservationId)
|
||||
.also { paymentRepository.save(it) }
|
||||
}
|
||||
|
||||
test("정상 반환") {
|
||||
paymentRepository.findByPaymentKey(payment.paymentKey)
|
||||
?.also {
|
||||
assertSoftly(it) {
|
||||
this.id shouldBe payment.id
|
||||
this.orderId shouldBe payment.orderId
|
||||
this.paymentKey shouldBe payment.paymentKey
|
||||
this.totalAmount shouldBe payment.totalAmount
|
||||
this.reservation.id shouldBe payment.reservation.id
|
||||
this.approvedAt shouldBe payment.approvedAt
|
||||
}
|
||||
}
|
||||
?: throw AssertionError("Unexpected null value")
|
||||
}
|
||||
|
||||
test("null 반환") {
|
||||
paymentRepository.findByPaymentKey("non-existent-key")
|
||||
.also { it shouldBe null }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupReservation(): Long {
|
||||
return ReservationFixture.create().also {
|
||||
entityManager.persist(it.member)
|
||||
entityManager.persist(it.theme)
|
||||
entityManager.persist(it.reservationTime)
|
||||
entityManager.persist(it)
|
||||
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
}.id
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,36 @@
|
||||
package roomescape.payment.web.support
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import io.kotest.assertions.assertSoftly
|
||||
import io.kotest.core.spec.style.StringSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import roomescape.payment.SampleTossPaymentConst
|
||||
import roomescape.payment.infrastructure.client.PaymentCancelResponseDeserializer
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
|
||||
class PaymentCancelResponseDeserializerTest : StringSpec({
|
||||
|
||||
val objectMapper: ObjectMapper = jacksonObjectMapper().registerModule(
|
||||
SimpleModule().addDeserializer(
|
||||
PaymentCancel.Response::class.java,
|
||||
PaymentCancelResponseDeserializer()
|
||||
)
|
||||
)
|
||||
|
||||
"결제 취소 응답을 역직렬화하여 PaymentCancelResponse 객체를 생성한다" {
|
||||
val cancelResponseJson: String = SampleTossPaymentConst.cancelJson
|
||||
val cancelResponse: PaymentCancel.Response = objectMapper.readValue(
|
||||
cancelResponseJson,
|
||||
PaymentCancel.Response::class.java
|
||||
)
|
||||
|
||||
assertSoftly(cancelResponse) {
|
||||
cancelResponse.cancelStatus shouldBe "DONE"
|
||||
cancelResponse.cancelReason shouldBe SampleTossPaymentConst.cancelReason
|
||||
cancelResponse.cancelAmount shouldBe SampleTossPaymentConst.amount
|
||||
cancelResponse.canceledAt.toString() shouldBe "2024-02-13T12:20:23+09:00"
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -2,6 +2,7 @@ package roomescape.reservation.controller;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ -35,15 +36,13 @@ import io.restassured.http.Header;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.payment.client.TossPaymentClient;
|
||||
import roomescape.payment.domain.CanceledPayment;
|
||||
import roomescape.payment.domain.Payment;
|
||||
import roomescape.payment.domain.repository.CanceledPaymentRepository;
|
||||
import roomescape.payment.domain.repository.PaymentRepository;
|
||||
import roomescape.payment.dto.request.PaymentCancelRequest;
|
||||
import roomescape.payment.dto.request.PaymentRequest;
|
||||
import roomescape.payment.dto.response.PaymentCancelResponse;
|
||||
import roomescape.payment.dto.response.PaymentResponse;
|
||||
import roomescape.payment.infrastructure.client.TossPaymentClient;
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity;
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository;
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity;
|
||||
import roomescape.payment.infrastructure.persistence.PaymentRepository;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
import roomescape.payment.web.PaymentCancel;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
@ -104,8 +103,8 @@ public class ReservationControllerTest {
|
||||
"paymentType", "DEFAULT"
|
||||
);
|
||||
|
||||
when(paymentClient.confirmPayment(any(PaymentRequest.class))).thenReturn(
|
||||
new PaymentResponse("pk", "oi", OffsetDateTime.of(date, time, ZoneOffset.ofHours(9)), 1000L));
|
||||
when(paymentClient.confirmPayment(any(PaymentApprove.Request.class))).thenReturn(
|
||||
new PaymentApprove.Response("pk", "oi", OffsetDateTime.of(date, time, ZoneOffset.ofHours(9)), 1000L));
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
@ -399,12 +398,12 @@ public class ReservationControllerTest {
|
||||
|
||||
Reservation saved = reservationRepository.save(
|
||||
new Reservation(date, time, theme, member, ReservationStatus.CONFIRMED));
|
||||
Payment savedPayment = paymentRepository.save(
|
||||
new Payment("pk", "oi", 1000L, saved, OffsetDateTime.now().minusHours(1L)));
|
||||
PaymentEntity savedPaymentEntity = paymentRepository.save(
|
||||
new PaymentEntity(null, "pk", "oi", 1000L, saved, OffsetDateTime.now().minusHours(1L)));
|
||||
|
||||
// when
|
||||
when(paymentClient.cancelPayment(any(PaymentCancelRequest.class)))
|
||||
.thenReturn(new PaymentCancelResponse("pk", "고객 요청", savedPayment.getTotalAmount(),
|
||||
when(paymentClient.cancelPayment(any(PaymentCancel.Request.class)))
|
||||
.thenReturn(new PaymentCancel.Response("pk", "고객 요청", savedPaymentEntity.getTotalAmount(),
|
||||
OffsetDateTime.now()));
|
||||
|
||||
// then
|
||||
@ -432,11 +431,11 @@ public class ReservationControllerTest {
|
||||
String paymentKey = "pk";
|
||||
OffsetDateTime canceledAt = OffsetDateTime.now().plusHours(1L).withNano(0);
|
||||
OffsetDateTime approvedAt = OffsetDateTime.of(localDateTime, ZoneOffset.ofHours(9));
|
||||
when(paymentClient.confirmPayment(any(PaymentRequest.class)))
|
||||
.thenReturn(new PaymentResponse(paymentKey, "oi", approvedAt, 1000L));
|
||||
when(paymentClient.confirmPayment(any(PaymentApprove.Request.class)))
|
||||
.thenReturn(new PaymentApprove.Response(paymentKey, "oi", approvedAt, 1000L));
|
||||
|
||||
when(paymentClient.cancelPayment(any(PaymentCancelRequest.class)))
|
||||
.thenReturn(new PaymentCancelResponse(paymentKey, "고객 요청", 1000L, canceledAt));
|
||||
when(paymentClient.cancelPayment(any(PaymentCancel.Request.class)))
|
||||
.thenReturn(new PaymentCancel.Response(paymentKey, "고객 요청", 1000L, canceledAt));
|
||||
|
||||
RestAssured.given().log().all()
|
||||
.contentType(ContentType.JSON)
|
||||
@ -448,12 +447,12 @@ public class ReservationControllerTest {
|
||||
.statusCode(400);
|
||||
|
||||
// then
|
||||
Optional<CanceledPayment> canceledPaymentOptional = canceledPaymentRepository.findByPaymentKey(paymentKey);
|
||||
assertThat(canceledPaymentOptional).isNotNull();
|
||||
assertThat(canceledPaymentOptional.get().getCanceledAt()).isEqualTo(canceledAt);
|
||||
assertThat(canceledPaymentOptional.get().getCancelReason()).isEqualTo("고객 요청");
|
||||
assertThat(canceledPaymentOptional.get().getCancelAmount()).isEqualTo(1000L);
|
||||
assertThat(canceledPaymentOptional.get().getApprovedAt()).isEqualTo(approvedAt);
|
||||
CanceledPaymentEntity canceledPayment = canceledPaymentRepository.findByPaymentKey(paymentKey);
|
||||
assertThat(canceledPayment).isNotNull();
|
||||
assertThat(canceledPayment.getCanceledAt()).isEqualTo(canceledAt);
|
||||
assertThat(canceledPayment.getCancelReason()).isEqualTo("고객 요청");
|
||||
assertThat(canceledPayment.getCancelAmount()).isEqualTo(1000L);
|
||||
assertThat(canceledPayment.getApprovedAt()).isEqualTo(approvedAt);
|
||||
}
|
||||
|
||||
@DisplayName("테마만을 이용하여 예약을 조회한다.")
|
||||
|
||||
@ -16,10 +16,11 @@ import org.springframework.test.context.jdbc.Sql.ExecutionPhase;
|
||||
import roomescape.member.infrastructure.persistence.Member;
|
||||
import roomescape.member.infrastructure.persistence.MemberRepository;
|
||||
import roomescape.member.infrastructure.persistence.Role;
|
||||
import roomescape.payment.domain.repository.CanceledPaymentRepository;
|
||||
import roomescape.payment.domain.repository.PaymentRepository;
|
||||
import roomescape.payment.dto.request.PaymentCancelRequest;
|
||||
import roomescape.payment.dto.response.PaymentResponse;
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentRepository;
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity;
|
||||
import roomescape.payment.infrastructure.persistence.PaymentRepository;
|
||||
import roomescape.payment.web.PaymentApprove;
|
||||
import roomescape.payment.web.PaymentCancel;
|
||||
import roomescape.reservation.domain.Reservation;
|
||||
import roomescape.reservation.domain.ReservationStatus;
|
||||
import roomescape.reservation.domain.ReservationTime;
|
||||
@ -53,7 +54,8 @@ class ReservationWithPaymentServiceTest {
|
||||
@DisplayName("예약과 결제 정보를 추가한다.")
|
||||
void addReservationWithPayment() {
|
||||
// given
|
||||
PaymentResponse paymentInfo = new PaymentResponse("payment-key", "order-id", OffsetDateTime.now(), 10000L);
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
@ -75,20 +77,21 @@ class ReservationWithPaymentServiceTest {
|
||||
assertThat(reservation.getReservationTime().getId()).isEqualTo(time.getId());
|
||||
assertThat(reservation.getReservationStatus()).isEqualTo(ReservationStatus.CONFIRMED);
|
||||
});
|
||||
paymentRepository.findByPaymentKey("payment-key")
|
||||
.ifPresent(payment -> {
|
||||
|
||||
PaymentEntity payment = paymentRepository.findByPaymentKey("payment-key");
|
||||
assertThat(payment).isNotNull();
|
||||
assertThat(payment.getReservation().getId()).isEqualTo(reservationResponse.id());
|
||||
assertThat(payment.getPaymentKey()).isEqualTo("payment-key");
|
||||
assertThat(payment.getOrderId()).isEqualTo("order-id");
|
||||
assertThat(payment.getTotalAmount()).isEqualTo(10000L);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("예약 ID를 이용하여 예약과 결제 정보를 제거하고, 결제 취소 정보를 저장한다.")
|
||||
void removeReservationWithPayment() {
|
||||
// given
|
||||
PaymentResponse paymentInfo = new PaymentResponse("payment-key", "order-id", OffsetDateTime.now(), 10000L);
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
@ -101,21 +104,22 @@ class ReservationWithPaymentServiceTest {
|
||||
reservationRequest, paymentInfo, member.getId());
|
||||
|
||||
// when
|
||||
PaymentCancelRequest paymentCancelRequest = reservationWithPaymentService.removeReservationWithPayment(
|
||||
PaymentCancel.Request paymentCancelRequest = reservationWithPaymentService.removeReservationWithPayment(
|
||||
reservationResponse.id(), member.getId());
|
||||
|
||||
// then
|
||||
assertThat(paymentCancelRequest.cancelReason()).isEqualTo("고객 요청");
|
||||
assertThat(paymentCancelRequest.cancelReason).isEqualTo("고객 요청");
|
||||
assertThat(reservationRepository.findById(reservationResponse.id())).isEmpty();
|
||||
assertThat(paymentRepository.findByPaymentKey("payment-key")).isEmpty();
|
||||
assertThat(canceledPaymentRepository.findByPaymentKey("payment-key")).isNotEmpty();
|
||||
assertThat(paymentRepository.findByPaymentKey("payment-key")).isNull();
|
||||
assertThat(canceledPaymentRepository.findByPaymentKey("payment-key")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("결제 정보가 없으면 True를 반환한다.")
|
||||
void isNotPaidReservation() {
|
||||
// given
|
||||
PaymentResponse paymentInfo = new PaymentResponse("payment-key", "order-id", OffsetDateTime.now(), 10000L);
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusHours(1L);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
@ -136,7 +140,8 @@ class ReservationWithPaymentServiceTest {
|
||||
@DisplayName("결제 정보가 있으면 False를 반환한다.")
|
||||
void isPaidReservation() {
|
||||
// given
|
||||
PaymentResponse paymentInfo = new PaymentResponse("payment-key", "order-id", OffsetDateTime.now(), 10000L);
|
||||
PaymentApprove.Response paymentInfo = new PaymentApprove.Response("payment-key", "order-id",
|
||||
OffsetDateTime.now(), 10000L);
|
||||
LocalDateTime localDateTime = LocalDateTime.now().plusDays(1L).withNano(0);
|
||||
LocalDate date = localDateTime.toLocalDate();
|
||||
ReservationTime time = reservationTimeRepository.save(new ReservationTime(localDateTime.toLocalTime()));
|
||||
|
||||
@ -1,37 +1,81 @@
|
||||
package roomescape.util
|
||||
|
||||
import roomescape.member.infrastructure.persistence.Member
|
||||
import roomescape.member.infrastructure.persistence.Role
|
||||
import roomescape.auth.infrastructure.jwt.JwtHandler
|
||||
import roomescape.auth.web.LoginRequest
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import roomescape.member.infrastructure.persistence.Member
|
||||
import roomescape.member.infrastructure.persistence.Role
|
||||
import roomescape.payment.infrastructure.persistence.CanceledPaymentEntity
|
||||
import roomescape.payment.infrastructure.persistence.PaymentEntity
|
||||
import roomescape.payment.web.PaymentApprove
|
||||
import roomescape.payment.web.PaymentCancel
|
||||
import roomescape.reservation.domain.Reservation
|
||||
import roomescape.reservation.domain.ReservationStatus
|
||||
import roomescape.reservation.domain.ReservationTime
|
||||
import roomescape.theme.domain.Theme
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
import java.time.OffsetDateTime
|
||||
import kotlin.random.Random
|
||||
|
||||
object MemberFixture {
|
||||
const val NOT_LOGGED_IN_USERID: Long = 0
|
||||
|
||||
val idCounter: AtomicLong = AtomicLong(1L)
|
||||
|
||||
fun create(
|
||||
id: Long? = idCounter.incrementAndGet(),
|
||||
id: Long? = null,
|
||||
name: String = "sangdol",
|
||||
account: String = "default",
|
||||
password: String = "password",
|
||||
role: Role = Role.ADMIN
|
||||
): Member = Member(id, name, "$account@email.com", password, role)
|
||||
|
||||
fun admin(): Member = create(account = "admin", role = Role.ADMIN)
|
||||
fun admin(): Member = create(
|
||||
id = 2L,
|
||||
account = "admin",
|
||||
role = Role.ADMIN
|
||||
)
|
||||
fun adminLoginRequest(): LoginRequest = LoginRequest(
|
||||
email = admin().email,
|
||||
password = admin().password
|
||||
)
|
||||
|
||||
fun user(): Member = create(account = "user", role = Role.MEMBER)
|
||||
fun user(): Member = create(
|
||||
id = 1L,
|
||||
account = "user",
|
||||
role = Role.MEMBER
|
||||
)
|
||||
fun userLoginRequest(): LoginRequest = LoginRequest(
|
||||
email = user().email,
|
||||
password = user().password
|
||||
)
|
||||
}
|
||||
|
||||
object ReservationTimeFixture {
|
||||
fun create(
|
||||
id: Long? = null,
|
||||
startAt: LocalTime = LocalTime.now().plusHours(1),
|
||||
): ReservationTime = ReservationTime(id, startAt)
|
||||
}
|
||||
|
||||
object ThemeFixture {
|
||||
fun create(
|
||||
id: Long? = null,
|
||||
name: String = "Default Theme",
|
||||
description: String = "Default Description",
|
||||
thumbnail: String = "https://example.com/default-thumbnail.jpg"
|
||||
): Theme = Theme(id, name, description, thumbnail)
|
||||
}
|
||||
|
||||
object ReservationFixture {
|
||||
fun create(
|
||||
id: Long? = null,
|
||||
date: LocalDate = LocalDate.now().plusWeeks(1),
|
||||
theme: Theme = ThemeFixture.create(),
|
||||
reservationTime: ReservationTime = ReservationTimeFixture.create(),
|
||||
member: Member = MemberFixture.create(),
|
||||
status: ReservationStatus = ReservationStatus.CONFIRMED_PAYMENT_REQUIRED
|
||||
): Reservation = Reservation(id, date, reservationTime, theme, member, status)
|
||||
}
|
||||
|
||||
object JwtFixture {
|
||||
const val SECRET_KEY: String = "daijawligagaf@LIJ$@U)9nagnalkkgalijaddljfi"
|
||||
const val EXPIRATION_TIME: Long = 1000 * 60 * 60
|
||||
@ -41,3 +85,70 @@ object JwtFixture {
|
||||
expirationTime: Long = EXPIRATION_TIME
|
||||
): JwtHandler = JwtHandler(secretKey, expirationTime)
|
||||
}
|
||||
|
||||
object PaymentFixture {
|
||||
const val PAYMENT_KEY: String = "paymentKey"
|
||||
const val ORDER_ID: String = "orderId"
|
||||
const val AMOUNT: Long = 10000L
|
||||
|
||||
fun create(
|
||||
id: Long? = null,
|
||||
orderId: String = ORDER_ID,
|
||||
paymentKey: String = PAYMENT_KEY,
|
||||
totalAmount: Long = AMOUNT,
|
||||
reservationId: Long = Random.nextLong(),
|
||||
approvedAt: OffsetDateTime = OffsetDateTime.now()
|
||||
): PaymentEntity = PaymentEntity(
|
||||
id = id,
|
||||
orderId = orderId,
|
||||
paymentKey = paymentKey,
|
||||
totalAmount = totalAmount,
|
||||
reservation = ReservationFixture.create(id = reservationId),
|
||||
approvedAt = approvedAt
|
||||
)
|
||||
|
||||
fun createCanceled(
|
||||
id: Long? = null,
|
||||
paymentKey: String = PAYMENT_KEY,
|
||||
cancelReason: String = "Test Cancel",
|
||||
cancelAmount: Long = AMOUNT,
|
||||
approvedAt: OffsetDateTime = OffsetDateTime.now(),
|
||||
canceledAt: OffsetDateTime = approvedAt.plusHours(1)
|
||||
): CanceledPaymentEntity = CanceledPaymentEntity(
|
||||
id = id,
|
||||
paymentKey = paymentKey,
|
||||
cancelReason = cancelReason,
|
||||
cancelAmount = cancelAmount,
|
||||
approvedAt = approvedAt,
|
||||
canceledAt = canceledAt
|
||||
|
||||
)
|
||||
|
||||
fun createApproveRequest(): PaymentApprove.Request = PaymentApprove.Request(
|
||||
paymentKey = PAYMENT_KEY,
|
||||
orderId = ORDER_ID,
|
||||
amount = AMOUNT,
|
||||
paymentType = "CARD"
|
||||
)
|
||||
|
||||
fun createApproveResponse(): PaymentApprove.Response = PaymentApprove.Response(
|
||||
paymentKey = PAYMENT_KEY,
|
||||
orderId = ORDER_ID,
|
||||
approvedAt = OffsetDateTime.now(),
|
||||
totalAmount = AMOUNT
|
||||
)
|
||||
|
||||
fun createCancelRequest(): PaymentCancel.Request = PaymentCancel.Request(
|
||||
paymentKey = PAYMENT_KEY,
|
||||
amount = AMOUNT,
|
||||
cancelReason = "Test Cancel"
|
||||
)
|
||||
|
||||
fun createCancelResponse(): PaymentCancel.Response = PaymentCancel.Response(
|
||||
cancelStatus = "SUCCESS",
|
||||
cancelReason = "Test Cancel",
|
||||
cancelAmount = AMOUNT,
|
||||
canceledAt = OffsetDateTime.now().plusMinutes(1)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -9,15 +9,6 @@ spring:
|
||||
sql:
|
||||
init:
|
||||
data-locations:
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
datasource:
|
||||
driver-class-name: org.h2.Driver
|
||||
url: jdbc:h2:mem:database-test
|
||||
username: sa
|
||||
password:
|
||||
|
||||
security:
|
||||
jwt:
|
||||
@ -25,3 +16,14 @@ security:
|
||||
secret-key: daijawligagaf@LIJ$@U)9nagnalkkgalijaddljfi
|
||||
access:
|
||||
expire-length: 1800000 # 30 분
|
||||
|
||||
payment:
|
||||
api-base-url: https://api.tosspayments.com
|
||||
confirm-secret-key: test_gsk_docs_OaPz8L5KdmQXkzRz3y47BMw6
|
||||
read-timeout: 3
|
||||
connect-timeout: 30
|
||||
|
||||
logging:
|
||||
level:
|
||||
org.springframework.orm.jpa: DEBUG
|
||||
org.springframework.transaction: DEBUG
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user