Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 결제 준비(생성) 기능 구현 #27

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d288960
chore: payment 스키마 생성
uijin31 Jan 20, 2025
79ee361
feat: 결제(Payment) 엔티티 생성
uijin31 Jan 20, 2025
c0a226a
refactor: 오타 수정(CANCELLED → CANCELED)
uijin31 Jan 20, 2025
8700b2f
feat: 결제 준비 API 엔드포인트 추가
uijin31 Jan 20, 2025
e6bcecd
feat: amount 타입 변경 (Long → Integer)
uijin31 Jan 20, 2025
e404de7
feat: ShortUUIDGenerator 유틸 클래스 생성
uijin31 Jan 20, 2025
87f5536
feat: BaseTimeEntity getter 추가
uijin31 Jan 20, 2025
bf64e1e
chore: 결제 관련 설정 추가 (ex. 결제 대기 시간)
uijin31 Jan 20, 2025
7d67d72
feat: 결제 준비 비즈니스 로직 구현
uijin31 Jan 20, 2025
61f59d6
chore: docker-compose name 설정 추가
uijin31 Jan 20, 2025
d3ca139
refactor: 불필요한 클래스 제거
uijin31 Jan 20, 2025
44272eb
rename: controller/api/dto → application/dto 이동
uijin31 Jan 20, 2025
3ba4225
rename: 변수명 변경(requestTime → requestAt)
uijin31 Jan 20, 2025
49de10a
refactor: 불필요한 Getter 제거
uijin31 Jan 20, 2025
fc6de2a
refactor: 테스트 용의성을 위해 LocalDateTime.now() 시 Clock 파라미터를 사용하도록 변경
uijin31 Jan 20, 2025
45e8b63
test: 테스트를 위한 더미 데이터 이름 변경
uijin31 Jan 20, 2025
bd1bc6d
test: 결제 준비 API 통합 테스트 작성
uijin31 Jan 20, 2025
a477884
test: ShortUUIDGenerator 단위 테스트 코드 작성
uijin31 Jan 20, 2025
64ba0a7
test: PaymentService 단위 테스트 작성
uijin31 Jan 20, 2025
f6442a3
test: BookingService 단위 테스트 추가
uijin31 Jan 20, 2025
ffb60b4
test: Clock 설정으로 인해 통합 테스트가 실행되지 않는 오류 해결
uijin31 Jan 20, 2025
a2f71f6
test: paymentToken 대신 paymentId를 반환하도록 변경
uijin31 Jan 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
version: '3.8'
name: nowait

services:
nowait-local-db:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ public BookingRes book(Long loginId, Long placeId, LocalDate date, LocalTime tim
return BookingRes.of(booking, slot);
}

public Booking getById(Long bookingId) {
return bookingRepository.findById(bookingId)
.orElseThrow(() -> new EntityNotFoundException("존재하지 않는 예약입니다."));
}

public BookingSlot getBookingSlotById(Long bookingSlotId) {
return bookingSlotRepository.findById(bookingSlotId)
.orElseThrow(() -> new EntityNotFoundException("예약 슬롯이 존재하지 않습니다."));
}

private boolean isAvailable(List<BookingSlot> slots) {
// 모든 슬롯이 예약된 경우에만 false 반환
return slots.stream().anyMatch(slot -> !slot.isBooked());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.nowait.application;

import com.nowait.domain.model.booking.Booking;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DepositService {

public void validateDepositAmount(Booking booking, Integer amount) {
// TODO: 해당 예약의 예약금과 amount가 같은지 확인하는 로직 추가
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.nowait.application;

import com.nowait.application.dto.response.payment.ReadyPaymentRes;
import com.nowait.config.PaymentProperties;
import com.nowait.domain.model.booking.Booking;
import com.nowait.domain.model.payment.Payment;
import com.nowait.domain.repository.PaymentRepository;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PaymentService {

private final PaymentProperties property;
private final PaymentRepository paymentRepository;
private final BookingService bookingService;
private final DepositService depositService;

@Transactional
public ReadyPaymentRes ready(Long loginId, Long bookingId, Integer amount,
LocalDateTime requestAt) {
// 1. 필요한 엔티티 조회
Booking booking = bookingService.getById(bookingId);

// 2-1. 검증 - 예약자 본인인지 확인
booking.validateOwner(loginId);
// 2-2. 검증 - 결제 금액이 예약금과 일치하는지 확인
depositService.validateDepositAmount(booking, amount);
// 2-3. 검증 - 결제 가능한 상태인지 확인
validateCanBookingReady(booking, requestAt);

// 3. 결제 생성 및 저장
Payment payment = paymentRepository.save(Payment.of(bookingId, loginId, amount));

return new ReadyPaymentRes(payment.getId());
}

private void validateCanBookingReady(Booking booking, LocalDateTime requestAt) {
// 1. 예약 상태가 '결제 대기 중'인지 확인
validatePayableBookingStatus(booking);

// 2. 결제 대기 시간이 지나지 않았는지 확인
if (isPassedPaymentWaitingTime(booking, requestAt)) {
throw new IllegalArgumentException("결제 대기 시간이 지났습니다.");
}
}

private void validatePayableBookingStatus(Booking booking) {
if (!booking.isPaymentAvailable()) {
throw new IllegalArgumentException("결제할 수 없는 예약입니다.");
}
}

private boolean isPassedPaymentWaitingTime(Booking booking, LocalDateTime requestAt) {
return requestAt.isAfter(
booking.getCreatedAt().plusHours(property.depositPaymentWaitHours()));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.nowait.application.dto.response.payment;

public record ReadyPaymentRes(
Long id
) {

}
16 changes: 16 additions & 0 deletions nowait-api/src/main/java/com/nowait/config/ClockConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.nowait.config;

import java.time.Clock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("!test")
public class ClockConfig {

@Bean
public Clock clock() {
return Clock.systemDefaultZone(); // 시스템 기본 타임존을 사용
}
}
11 changes: 11 additions & 0 deletions nowait-api/src/main/java/com/nowait/config/PaymentProperties.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.nowait.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("payment")
public record PaymentProperties(
int depositPaymentWaitHours,
int approvalWaitMinutes
) {

}
10 changes: 10 additions & 0 deletions nowait-api/src/main/java/com/nowait/config/PropertyScanConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.nowait.config;

import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConfigurationPropertiesScan
public class PropertyScanConfig {

}
34 changes: 23 additions & 11 deletions nowait-api/src/main/java/com/nowait/controller/api/PaymentApi.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.nowait.controller.api;

import com.nowait.application.dto.response.payment.PayDepositRes;
import com.nowait.controller.api.dto.request.PayDepositReq;
import com.nowait.application.PaymentService;
import com.nowait.application.dto.response.payment.ReadyPaymentRes;
import com.nowait.controller.api.dto.request.ReadyPaymentReq;
import com.nowait.controller.api.dto.response.ApiResult;
import jakarta.validation.Valid;
import java.time.Clock;
import java.time.LocalDateTime;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -15,18 +20,25 @@
@RequiredArgsConstructor
public class PaymentApi {

private final PaymentService paymentService;
private final ExecutorService executorService;
private final Clock clock;

/**
* 예약금 결제 API
* 결제 준비 요청 API
*
* @param request 예약금 결제 요청
* @return 예약금 결제 결과
* @param request 결제 준비 요청 (bookingId, amount)
* @return 결제 토큰
*/
@PostMapping("/deposit")
public ApiResult<PayDepositRes> payDeposit(
@RequestBody @Valid PayDepositReq request
@PostMapping("/ready")
public CompletableFuture<ApiResult<ReadyPaymentRes>> ready(
@RequestBody @Valid ReadyPaymentReq request
) {
// TODO: 예약금 결제 비즈니스 로직 호출

return ApiResult.ok(null);
// TODO: Auth 기능 구현 시 loginId를 Authentication에서 가져오도록 수정
Long loginId = 1L;
return CompletableFuture.supplyAsync(
() -> paymentService.ready(loginId, request.bookingId(), request.amount(),
LocalDateTime.now(clock)), executorService)
.thenApply(ApiResult::ok);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.nowait.controller.api.dto.request;

import jakarta.validation.constraints.NotNull;

public record ReadyPaymentReq(
@NotNull
Long bookingId,
@NotNull
Integer amount
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.LocalDateTime;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public class BaseTimeEntity {

@CreatedDate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ public static Booking of(Long userId, BookingSlot slot, Integer partySize) {
);
}

public void validateOwner(Long loginId) {
if (!userId.equals(loginId)) {
throw new IllegalArgumentException("예약자가 아닙니다.");
}
}

public boolean isPaymentAvailable() {
return status == BookingStatus.PENDING_PAYMENT;
}

private static void validateUserId(Long userId) {
requireNonNull(userId, "예약자 식별자는 필수값입니다.");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public enum BookingStatus {
CONFIRMED("확정됨"),
PENDING_CONFIRM("확정 대기 중"),
PENDING_PAYMENT("결제 대기 중"),
CANCELLED("취소됨");
CANCELED("취소됨");

private final String description;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.nowait.domain.model.payment;

import com.nowait.domain.model.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "payment")
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Payment extends BaseTimeEntity {

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

@Column(name = "booking_id", nullable = false)
private Long bookingId;

@Column(name = "user_id", nullable = false)
private Long userId;

@Column(name = "payment_key")
private String paymentKey;

@Enumerated(value = EnumType.STRING)
@Column(name = "status", nullable = false)
private PaymentStatus status;

@Column(name = "amount", nullable = false)
private Integer amount;

public static Payment of(Long bookingId, Long userId, Integer amount) {
return new Payment(null, bookingId, userId, null, PaymentStatus.READY, amount);
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.nowait.domain.model.payment;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum PaymentStatus {
READY("결제 준비 중"),
IN_PROGRESS("승인 진행 중"),
DONE("승인 완료"),
CANCELED("취소됨"),
ABORTED("승인 실패"),
EXPIRED("유효 기간 만료됨");

private final String description;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.nowait.domain.repository;

import com.nowait.domain.model.payment.Payment;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PaymentRepository extends JpaRepository<Payment, Long> {

}
4 changes: 4 additions & 0 deletions nowait-api/src/main/resources/application-local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,7 @@ spring:

flyway:
enabled: true

payment:
deposit-payment-wait-hours: 2
approval-wait-minutes: 10
12 changes: 12 additions & 0 deletions nowait-api/src/main/resources/db/migration/V2__create_payment.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Payment
CREATE TABLE payment
(
id BIGINT PRIMARY KEY AUTO_INCREMENT,
booking_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
payment_key VARCHAR(255),
status VARCHAR(30) NOT NULL,
amount INT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON update CURRENT_TIMESTAMP
);
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.nowait;

import com.nowait.config.TestConfig;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@SpringBootTest
@Import(TestConfig.class)
class NowaitApplicationTests {

@Test
Expand Down
Loading
Loading