Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
CLAUDE.md

### Docs ###
**/TEST_LIST.md
**/*.md

### Gradle ###
.gradle/
Expand Down
9 changes: 9 additions & 0 deletions buildSrc/src/main/groovy/paybook.java-conventions.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id 'java'
id 'io.spring.dependency-management'
id 'jacoco'
}

group = 'com.paybook'
Expand Down Expand Up @@ -31,4 +32,12 @@ dependencyManagement {

test {
useJUnitPlatform()
finalizedBy jacocoTestReport
}

jacocoTestReport {
dependsOn test
reports {
csv.required = true
}
}
3 changes: 3 additions & 0 deletions core/src/main/java/com/paybook/core/entity/CouponEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public class CouponEntity {

private Integer minOrderAmount;

@Version
private Long version;

public CouponEntity(String couponId, CouponStatus status, CouponType couponType,
DiscountType discountType, int discountValue, Integer maxDiscountAmount) {
this(couponId, status, couponType, discountType, discountValue, maxDiscountAmount, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class PaymentEntity {
@Column(nullable = false, unique = true)
private String paymentId;

@Column(nullable = false)
@Column(nullable = false, unique = true)
private String orderId;

private int pgPaymentAmount;
Expand All @@ -34,6 +34,9 @@ public class PaymentEntity {
@Column(nullable = false)
private LocalDateTime createdAt;

@Version
private Long version;

public PaymentEntity(String paymentId, String orderId, int pgPaymentAmount) {
this.paymentId = paymentId;
this.orderId = orderId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public class UserPointEntity {

private int balance;

@Version
private Long version;

public UserPointEntity(String userId, int balance) {
this.userId = userId;
this.balance = balance;
Expand Down
41 changes: 41 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
services:
orderdb:
image: postgres:17
container_name: paybook-orderdb
environment:
POSTGRES_DB: orderdb
POSTGRES_USER: paybook
POSTGRES_PASSWORD: paybook
ports:
- "5432:5432"
volumes:
- orderdb-data:/var/lib/postgresql/data

paymentdb:
image: postgres:17
container_name: paybook-paymentdb
environment:
POSTGRES_DB: paymentdb
POSTGRES_USER: paybook
POSTGRES_PASSWORD: paybook
ports:
- "5433:5432"
volumes:
- paymentdb-data:/var/lib/postgresql/data

settlementdb:
image: postgres:17
container_name: paybook-settlementdb
environment:
POSTGRES_DB: settlementdb
POSTGRES_USER: paybook
POSTGRES_PASSWORD: paybook
ports:
- "5434:5432"
volumes:
- settlementdb-data:/var/lib/postgresql/data

volumes:
orderdb-data:
paymentdb-data:
settlementdb-data:
3 changes: 2 additions & 1 deletion order/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'org.postgresql:postgresql'
testRuntimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public record OrderResponse(
int pgPaymentAmount,
int deliveryFee,
String status,
String couponId,
String createdAt
) {
public record OrderItemResponse(
Expand Down
34 changes: 11 additions & 23 deletions order/src/main/java/com/paybook/order/entity/OrderEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

import com.paybook.order.exception.OrderException;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
Expand All @@ -16,6 +14,8 @@
@Table(name = "orders")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public class OrderEntity {

private static final Set<OrderStatus> NON_CANCELLABLE_STATUSES = EnumSet.of(
Expand All @@ -34,6 +34,7 @@ public class OrderEntity {
private String userId;

@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<OrderItemEntity> items = new ArrayList<>();

private int totalAmount;
Expand All @@ -48,7 +49,8 @@ public class OrderEntity {

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private OrderStatus status;
@Builder.Default
private OrderStatus status = OrderStatus.PENDING_PAYMENT;

private String deliveryAddress;

Expand All @@ -57,25 +59,11 @@ public class OrderEntity {
private Integer pointAmountToUse;

@Column(nullable = false)
private LocalDateTime createdAt;

public OrderEntity(String orderId, String userId, int totalAmount,
int couponDiscountAmount, int pointDiscountAmount, int pgPaymentAmount,
int deliveryFee, String deliveryAddress, String couponId,
Integer pointAmountToUse) {
this.orderId = orderId;
this.userId = userId;
this.totalAmount = totalAmount;
this.couponDiscountAmount = couponDiscountAmount;
this.pointDiscountAmount = pointDiscountAmount;
this.pgPaymentAmount = pgPaymentAmount;
this.deliveryFee = deliveryFee;
this.deliveryAddress = deliveryAddress;
this.couponId = couponId;
this.pointAmountToUse = pointAmountToUse;
this.status = OrderStatus.PENDING_PAYMENT;
this.createdAt = LocalDateTime.now();
}
@Builder.Default
private LocalDateTime createdAt = LocalDateTime.now();

@Version
private Long version;

public void addItem(OrderItemEntity item) {
items.add(item);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import org.springframework.data.jpa.repository.JpaRepository;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

public interface OrderRepository extends JpaRepository<OrderEntity, Long> {
Expand All @@ -18,5 +17,5 @@ public interface OrderRepository extends JpaRepository<OrderEntity, Long> {

Page<OrderEntity> findByUserIdAndStatusOrderByCreatedAtDesc(String userId, OrderStatus status, Pageable pageable);

List<OrderEntity> findByStatusAndCreatedAtBefore(OrderStatus status, LocalDateTime cutoff);
Page<OrderEntity> findByStatusAndCreatedAtBefore(OrderStatus status, LocalDateTime cutoff, Pageable pageable);
}
27 changes: 16 additions & 11 deletions order/src/main/java/com/paybook/order/service/OrderService.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,14 @@
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.UUID;
import java.util.stream.IntStream;

@Service
@RequiredArgsConstructor
public class OrderService {

private static final int PERCENT_DIVISOR = 100;
private static final int ORDER_ID_PAD_LENGTH = 6;

private final AtomicLong sequence = new AtomicLong(1);

private final OrderRepository orderRepository;
private final ProductRepository productRepository;
Expand Down Expand Up @@ -261,13 +258,20 @@ private void validateMinPgPayment(int totalAmount, int pgPaymentAmount, int deli
private OrderEntity buildAndSaveOrder(CreateOrderRequest request, List<ProductEntity> products,
int totalAmount, int couponDiscount, int pointDiscount,
int pgPaymentAmount, int deliveryFee) {
String orderId = "ORD-" + String.format("%0" + ORDER_ID_PAD_LENGTH + "d", sequence.getAndIncrement());

OrderEntity order = new OrderEntity(
orderId, request.userId(), totalAmount,
couponDiscount, pointDiscount, pgPaymentAmount,
deliveryFee, request.deliveryAddress(), request.couponId(), request.pointAmountToUse()
);
String orderId = "ORD-" + UUID.randomUUID().toString().substring(0, 8);

OrderEntity order = OrderEntity.builder()
.orderId(orderId)
.userId(request.userId())
.totalAmount(totalAmount)
.couponDiscountAmount(couponDiscount)
.pointDiscountAmount(pointDiscount)
.pgPaymentAmount(pgPaymentAmount)
.deliveryFee(deliveryFee)
.deliveryAddress(request.deliveryAddress())
.couponId(request.couponId())
.pointAmountToUse(request.pointAmountToUse())
.build();

IntStream.range(0, request.items().size()).forEach(i -> {
CreateOrderRequest.OrderItemRequest itemReq = request.items().get(i);
Expand Down Expand Up @@ -335,6 +339,7 @@ private OrderResponse toResponse(OrderEntity order) {
order.getTotalAmount(), order.getCouponDiscountAmount(),
order.getPointDiscountAmount(), order.getPgPaymentAmount(),
order.getDeliveryFee(), order.getStatus().name(),
order.getCouponId(),
order.getCreatedAt().toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,42 +6,55 @@
import com.paybook.order.repository.OrderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.List;

@Slf4j
@Component
@RequiredArgsConstructor
public class PaymentTimeoutScheduler {

private static final int BATCH_SIZE = 500;

private final OrderRepository orderRepository;
private final OrderService orderService;
private final PaymentTimeoutConfig paymentTimeoutConfig;

@Scheduled(fixedDelayString = "${order.payment.timeout-check-interval:60000}")
public void cancelTimedOutOrders() {
LocalDateTime cutoff = LocalDateTime.now().minusMinutes(paymentTimeoutConfig.timeoutMinutes());
List<OrderEntity> timedOutOrders = orderRepository.findByStatusAndCreatedAtBefore(
OrderStatus.PENDING_PAYMENT, cutoff);

timedOutOrders.forEach(order -> {
try {
orderService.cancelOrder(order.getOrderId());
log.info("결제 타임아웃 주문 자동 취소: {}", order.getOrderId());
} catch (Exception e) {
log.error("결제 타임아웃 주문 취소 실패: {}", order.getOrderId(), e);
}
});
int cancelled = processBatch(cutoff);
if (cancelled > 0) {
log.info("결제 타임아웃 주문 자동 취소: {}건", cancelled);
}
}

public int cancelTimedOutOrdersManually(LocalDateTime cutoff) {
List<OrderEntity> timedOutOrders = orderRepository.findByStatusAndCreatedAtBefore(
OrderStatus.PENDING_PAYMENT, cutoff);
return processBatch(cutoff);
}

private int processBatch(LocalDateTime cutoff) {
int totalCancelled = 0;
Page<OrderEntity> page;

do {
page = orderRepository.findByStatusAndCreatedAtBefore(
OrderStatus.PENDING_PAYMENT, cutoff, PageRequest.of(0, BATCH_SIZE));

for (OrderEntity order : page.getContent()) {
try {
orderService.cancelOrder(order.getOrderId());
totalCancelled++;
} catch (Exception e) {
log.error("결제 타임아웃 주문 취소 실패: {}", order.getOrderId(), e);
}
}
} while (page.hasNext());

timedOutOrders.forEach(order -> orderService.cancelOrder(order.getOrderId()));
return timedOutOrders.size();
return totalCancelled;
}
}
10 changes: 5 additions & 5 deletions order/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ spring:
application:
name: order-service
datasource:
url: jdbc:h2:mem:orderdb
driver-class-name: org.h2.Driver
username: sa
password:
url: jdbc:postgresql://localhost:5432/orderdb
driver-class-name: org.postgresql.Driver
username: paybook
password: paybook
jpa:
hibernate:
ddl-auto: create-drop
ddl-auto: update
open-in-view: false

server:
Expand Down
Loading
Loading