카드 매입 승인 경로 Spring Boot 3 전환 #5
8 changed files with 673 additions and 0 deletions
90
pom.xml
Normal file
90
pom.xml
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>com.acquirex</groupId>
|
||||
<artifactId>proj-acquirex</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>proj-acquirex</name>
|
||||
<description>카드 매입 승인 경로 - Spring Boot 3 Migration</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.persistence</groupId>
|
||||
<artifactId>jakarta.persistence-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>jakarta.validation</groupId>
|
||||
<artifactId>jakarta.validation-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
20
src/main/java/com/acquirex/AcquirexApplication.java
Normal file
20
src/main/java/com/acquirex/AcquirexApplication.java
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
package com.acquirex;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 카드 매입 승인 경로 Spring Boot 3 애플리케이션
|
||||
*
|
||||
* Spring Boot 3.x 마이그레이션:
|
||||
* - Java 17+ 필수
|
||||
* - Jakarta EE 9+ (javax.* → jakarta.*)
|
||||
* - Spring Framework 6.x
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class AcquirexApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AcquirexApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.acquirex.controller;
|
||||
|
||||
import com.acquirex.service.AcquisitionRequest;
|
||||
import com.acquirex.service.AcquisitionResponse;
|
||||
import com.acquirex.service.AcquisitionService;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 카드 매입 승인 REST 컨트롤러 - Spring Boot 3 (jakarta.validation)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/acquisitions")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AcquisitionController {
|
||||
|
||||
private final AcquisitionService acquisitionService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<AcquisitionResponse> registerAcquisition(@Valid @RequestBody AcquisitionRequest request) {
|
||||
log.info("매입 요청 수신: {}", request);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(acquisitionService.registerAcquisition(request));
|
||||
}
|
||||
|
||||
@PostMapping("/{transactionId}/approve")
|
||||
public ResponseEntity<AcquisitionResponse> approveAcquisition(@PathVariable String transactionId) {
|
||||
log.info("매입 승인 요청: transactionId={}", transactionId);
|
||||
return ResponseEntity.ok(acquisitionService.approveAcquisition(transactionId));
|
||||
}
|
||||
|
||||
@PostMapping("/{transactionId}/reject")
|
||||
public ResponseEntity<AcquisitionResponse> rejectAcquisition(@PathVariable String transactionId, @RequestBody Map<String, String> body) {
|
||||
log.info("매입 거절 요청: transactionId={}", transactionId);
|
||||
return ResponseEntity.ok(acquisitionService.rejectAcquisition(transactionId, body.getOrDefault("reason", "사유 미지정")));
|
||||
}
|
||||
|
||||
@GetMapping("/{transactionId}")
|
||||
public ResponseEntity<AcquisitionResponse> getAcquisition(@PathVariable String transactionId) {
|
||||
log.info("매입 조회 요청: transactionId={}", transactionId);
|
||||
return ResponseEntity.ok(acquisitionService.getAcquisition(transactionId));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<AcquisitionResponse>> getAcquisitions(@RequestParam(required = false) String merchantId) {
|
||||
log.info("매입 목록 조회: merchantId={}", merchantId);
|
||||
List<AcquisitionResponse> responses = (merchantId != null && !merchantId.isBlank())
|
||||
? acquisitionService.getAcquisitionsByMerchant(merchantId)
|
||||
: acquisitionService.getPendingAcquisitions();
|
||||
return ResponseEntity.ok(responses);
|
||||
}
|
||||
}
|
||||
87
src/main/java/com/acquirex/domain/Acquisition.java
Normal file
87
src/main/java/com/acquirex/domain/Acquisition.java
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package com.acquirex.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import lombok.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 카드 매입 정보 도메인 엔티티
|
||||
* Jakarta EE 9+ 어노테이션 사용 (Spring Boot 3)
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "acquisitions")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Acquisition {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "가맹점 번호는 필수입니다")
|
||||
@Column(name = "merchant_id", nullable = false, length = 20)
|
||||
private String merchantId;
|
||||
|
||||
@NotBlank(message = "카드번호는 필수입니다")
|
||||
@Column(name = "card_number", nullable = false, length = 19)
|
||||
private String cardNumber;
|
||||
|
||||
@NotNull(message = "매입 금액은 필수입니다")
|
||||
@Positive(message = "매입 금액은 양수여야 합니다")
|
||||
@Column(name = "amount", nullable = false, precision = 15, scale = 2)
|
||||
private BigDecimal amount;
|
||||
|
||||
@NotBlank(message = "거래 번호는 필수입니다")
|
||||
@Column(name = "transaction_id", nullable = false, unique = true, length = 30)
|
||||
private String transactionId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private AcquisitionStatus status;
|
||||
|
||||
@Column(name = "approval_code", length = 10)
|
||||
private String approvalCode;
|
||||
|
||||
@Column(name = "approved_at")
|
||||
private LocalDateTime approvedAt;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void approve(String approvalCode) {
|
||||
this.status = AcquisitionStatus.APPROVED;
|
||||
this.approvalCode = approvalCode;
|
||||
this.approvedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void reject(String reason) {
|
||||
this.status = AcquisitionStatus.REJECTED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 매입 승인 상태 열거형
|
||||
*/
|
||||
enum AcquisitionStatus {
|
||||
PENDING, APPROVED, REJECTED, CANCELLED
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.acquirex.exception;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.validation.FieldError;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 전역 예외 처리 핸들러 - Spring Boot 3 (jakarta.validation)
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(AcquisitionNotFoundException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleAcquisitionNotFound(AcquisitionNotFoundException ex) {
|
||||
log.error("매입 정보 없음: {}", ex.getMessage());
|
||||
return buildErrorResponse(HttpStatus.NOT_FOUND, ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(InvalidAcquisitionStateException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleInvalidAcquisitionState(InvalidAcquisitionStateException ex) {
|
||||
log.error("유효하지 않은 매입 상태: {}", ex.getMessage());
|
||||
return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleValidationErrors(MethodArgumentNotValidException ex) {
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
ex.getBindingResult().getAllErrors().forEach(error -> {
|
||||
String fieldName = ((FieldError) error).getField();
|
||||
errors.put(fieldName, error.getDefaultMessage());
|
||||
});
|
||||
log.error("검증 오류: {}", errors);
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("timestamp", LocalDateTime.now());
|
||||
response.put("status", HttpStatus.BAD_REQUEST.value());
|
||||
response.put("error", "Validation Failed");
|
||||
response.put("errors", errors);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<Map<String, Object>> handleGenericException(Exception ex) {
|
||||
log.error("예상치 못한 오류 발생", ex);
|
||||
return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "예상치 못한 오류가 발생했습니다");
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, Object>> buildErrorResponse(HttpStatus status, String message) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("timestamp", LocalDateTime.now());
|
||||
response.put("status", status.value());
|
||||
response.put("error", status.getReasonPhrase());
|
||||
response.put("message", message);
|
||||
return ResponseEntity.status(status).body(response);
|
||||
}
|
||||
}
|
||||
|
||||
/** 매입 정보를 찾을 수 없을 때 발생하는 예외 */
|
||||
class AcquisitionNotFoundException extends RuntimeException {
|
||||
public AcquisitionNotFoundException(String message) { super(message); }
|
||||
}
|
||||
|
||||
/** 매입 상태가 유효하지 않을 때 발생하는 예외 */
|
||||
class InvalidAcquisitionStateException extends RuntimeException {
|
||||
public InvalidAcquisitionStateException(String message) { super(message); }
|
||||
}
|
||||
145
src/main/java/com/acquirex/service/AcquisitionService.java
Normal file
145
src/main/java/com/acquirex/service/AcquisitionService.java
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
package com.acquirex.service;
|
||||
|
||||
import com.acquirex.domain.Acquisition;
|
||||
import com.acquirex.domain.AcquisitionStatus;
|
||||
import com.acquirex.exception.AcquisitionNotFoundException;
|
||||
import com.acquirex.exception.InvalidAcquisitionStateException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 카드 매입 승인 서비스 - Spring Boot 3 트랜잭션 관리
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AcquisitionService {
|
||||
|
||||
private final AcquisitionRepository acquisitionRepository;
|
||||
|
||||
@Transactional
|
||||
public AcquisitionResponse registerAcquisition(AcquisitionRequest request) {
|
||||
log.info("매입 요청 등록: merchantId={}, amount={}", request.getMerchantId(), request.getAmount());
|
||||
String transactionId = generateTransactionId();
|
||||
|
||||
Acquisition acquisition = Acquisition.builder()
|
||||
.merchantId(request.getMerchantId())
|
||||
.cardNumber(maskCardNumber(request.getCardNumber()))
|
||||
.amount(request.getAmount())
|
||||
.transactionId(transactionId)
|
||||
.status(AcquisitionStatus.PENDING)
|
||||
.build();
|
||||
|
||||
Acquisition saved = acquisitionRepository.save(acquisition);
|
||||
log.info("매입 요청 등록 완료: transactionId={}", transactionId);
|
||||
return toResponse(saved);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AcquisitionResponse approveAcquisition(String transactionId) {
|
||||
log.info("매입 승인 처리: transactionId={}", transactionId);
|
||||
|
||||
Acquisition acquisition = acquisitionRepository.findByTransactionId(transactionId)
|
||||
.orElseThrow(() -> new AcquisitionNotFoundException("매입 정보를 찾을 수 없습니다: " + transactionId));
|
||||
|
||||
if (acquisition.getStatus() != AcquisitionStatus.PENDING) {
|
||||
throw new InvalidAcquisitionStateException("대기 상태의 매입만 승인할 수 있습니다. 현재 상태: " + acquisition.getStatus());
|
||||
}
|
||||
|
||||
String approvalCode = generateApprovalCode();
|
||||
acquisition.approve(approvalCode);
|
||||
Acquisition saved = acquisitionRepository.save(acquisition);
|
||||
log.info("매입 승인 완료: transactionId={}, approvalCode={}", transactionId, approvalCode);
|
||||
return toResponse(saved);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AcquisitionResponse rejectAcquisition(String transactionId, String reason) {
|
||||
log.info("매입 거절 처리: transactionId={}, reason={}", transactionId, reason);
|
||||
|
||||
Acquisition acquisition = acquisitionRepository.findByTransactionId(transactionId)
|
||||
.orElseThrow(() -> new AcquisitionNotFoundException("매입 정보를 찾을 수 없습니다: " + transactionId));
|
||||
|
||||
if (acquisition.getStatus() != AcquisitionStatus.PENDING) {
|
||||
throw new InvalidAcquisitionStateException("대기 상태의 매입만 거절할 수 있습니다. 현재 상태: " + acquisition.getStatus());
|
||||
}
|
||||
|
||||
acquisition.reject(reason);
|
||||
return toResponse(acquisitionRepository.save(acquisition));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public AcquisitionResponse getAcquisition(String transactionId) {
|
||||
return toResponse(acquisitionRepository.findByTransactionId(transactionId)
|
||||
.orElseThrow(() -> new AcquisitionNotFoundException("매입 정보를 찾을 수 없습니다: " + transactionId)));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<AcquisitionResponse> getAcquisitionsByMerchant(String merchantId) {
|
||||
return acquisitionRepository.findByMerchantId(merchantId).stream().map(this::toResponse).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<AcquisitionResponse> getPendingAcquisitions() {
|
||||
return acquisitionRepository.findByStatus(AcquisitionStatus.PENDING).stream().map(this::toResponse).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private String generateTransactionId() {
|
||||
return "TXN" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||
}
|
||||
|
||||
private String generateApprovalCode() {
|
||||
return String.format("%06d", (int) (Math.random() * 1000000));
|
||||
}
|
||||
|
||||
private String maskCardNumber(String cardNumber) {
|
||||
if (cardNumber == null || cardNumber.length() < 10) return cardNumber;
|
||||
return cardNumber.substring(0, 6) + "****" + cardNumber.substring(cardNumber.length() - 4);
|
||||
}
|
||||
|
||||
private AcquisitionResponse toResponse(Acquisition a) {
|
||||
return AcquisitionResponse.builder()
|
||||
.id(a.getId()).transactionId(a.getTransactionId()).merchantId(a.getMerchantId())
|
||||
.cardNumber(a.getCardNumber()).amount(a.getAmount()).status(a.getStatus())
|
||||
.approvalCode(a.getApprovalCode()).approvedAt(a.getApprovedAt())
|
||||
.createdAt(a.getCreatedAt()).updatedAt(a.getUpdatedAt()).build();
|
||||
}
|
||||
}
|
||||
|
||||
/** DTOs */
|
||||
@lombok.Getter @lombok.Setter @lombok.NoArgsConstructor @lombok.AllArgsConstructor @lombok.Builder
|
||||
class AcquisitionRequest {
|
||||
@jakarta.validation.constraints.NotBlank @jakarta.validation.constraints.Size(max = 20)
|
||||
private String merchantId;
|
||||
@jakarta.validation.constraints.NotBlank @jakarta.validation.constraints.Pattern(regexp = "\\d{13,19}")
|
||||
private String cardNumber;
|
||||
@jakarta.validation.constraints.NotNull @jakarta.validation.constraints.DecimalMin("0.01")
|
||||
private BigDecimal amount;
|
||||
}
|
||||
|
||||
@lombok.Getter @lombok.Setter @lombok.NoArgsConstructor @lombok.AllArgsConstructor @lombok.Builder
|
||||
class AcquisitionResponse {
|
||||
private Long id; private String transactionId; private String merchantId;
|
||||
private String cardNumber; private BigDecimal amount; private AcquisitionStatus status;
|
||||
private String approvalCode; private LocalDateTime approvedAt;
|
||||
private LocalDateTime createdAt; private LocalDateTime updatedAt;
|
||||
}
|
||||
|
||||
/** Repository */
|
||||
@Repository
|
||||
interface AcquisitionRepository extends JpaRepository<Acquisition, Long> {
|
||||
Optional<Acquisition> findByTransactionId(String transactionId);
|
||||
List<Acquisition> findByMerchantId(String merchantId);
|
||||
List<Acquisition> findByStatus(AcquisitionStatus status);
|
||||
}
|
||||
38
src/main/resources/application.yml
Normal file
38
src/main/resources/application.yml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
spring:
|
||||
application:
|
||||
name: proj-acquirex
|
||||
datasource:
|
||||
url: jdbc:h2:mem:acquirex;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
dialect: org.hibernate.dialect.H2Dialect
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,info,metrics
|
||||
endpoint:
|
||||
health:
|
||||
show-details: when_authorized
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.acquirex: DEBUG
|
||||
org.springframework.web: INFO
|
||||
org.hibernate.SQL: DEBUG
|
||||
160
src/test/java/com/acquirex/AcquisitionServiceTest.java
Normal file
160
src/test/java/com/acquirex/AcquisitionServiceTest.java
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
package com.acquirex;
|
||||
|
||||
import com.acquirex.domain.Acquisition;
|
||||
import com.acquirex.domain.AcquisitionStatus;
|
||||
import com.acquirex.exception.AcquisitionNotFoundException;
|
||||
import com.acquirex.exception.InvalidAcquisitionStateException;
|
||||
import com.acquirex.service.AcquisitionRequest;
|
||||
import com.acquirex.service.AcquisitionResponse;
|
||||
import com.acquirex.service.AcquisitionService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* AcquisitionService 단위 테스트 - Spring Boot 3 마이그레이션 검증
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class AcquisitionServiceTest {
|
||||
|
||||
@Mock
|
||||
private com.acquirex.service.AcquisitionRepository acquisitionRepository;
|
||||
|
||||
private AcquisitionService acquisitionService;
|
||||
private AcquisitionRequest validRequest;
|
||||
private Acquisition pendingAcquisition;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
acquisitionService = new AcquisitionService(acquisitionRepository);
|
||||
validRequest = AcquisitionRequest.builder()
|
||||
.merchantId("MERCHANT001")
|
||||
.cardNumber("1234567890123456")
|
||||
.amount(new BigDecimal("50000.00"))
|
||||
.build();
|
||||
|
||||
pendingAcquisition = Acquisition.builder()
|
||||
.id(1L).merchantId("MERCHANT001").cardNumber("123456****3456")
|
||||
.amount(new BigDecimal("50000.00")).transactionId("TXN123456789")
|
||||
.status(AcquisitionStatus.PENDING).createdAt(LocalDateTime.now()).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("매입 요청 등록 성공 - Spring Boot 3 Jakarta Validation")
|
||||
void registerAcquisition_Success() {
|
||||
when(acquisitionRepository.save(any(Acquisition.class))).thenAnswer(invocation -> {
|
||||
Acquisition saved = invocation.getArgument(0);
|
||||
saved.setId(1L);
|
||||
saved.setCreatedAt(LocalDateTime.now());
|
||||
return saved;
|
||||
});
|
||||
|
||||
AcquisitionResponse response = acquisitionService.registerAcquisition(validRequest);
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getMerchantId()).isEqualTo("MERCHANT001");
|
||||
assertThat(response.getAmount()).isEqualByComparingTo(new BigDecimal("50000.00"));
|
||||
assertThat(response.getStatus()).isEqualTo(AcquisitionStatus.PENDING);
|
||||
assertThat(response.getCardNumber()).isEqualTo("123456****3456");
|
||||
verify(acquisitionRepository, times(1)).save(any(Acquisition.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("매입 승인 처리 성공 - Spring Boot 3 @Transactional")
|
||||
void approveAcquisition_Success() {
|
||||
when(acquisitionRepository.findByTransactionId("TXN123456789")).thenReturn(Optional.of(pendingAcquisition));
|
||||
when(acquisitionRepository.save(any(Acquisition.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
AcquisitionResponse response = acquisitionService.approveAcquisition("TXN123456789");
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatus()).isEqualTo(AcquisitionStatus.APPROVED);
|
||||
assertThat(response.getApprovalCode()).isNotNull();
|
||||
assertThat(response.getApprovalCode()).hasSize(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("매입 승인 실패 - 매입 정보 없음")
|
||||
void approveAcquisition_NotFound() {
|
||||
when(acquisitionRepository.findByTransactionId("INVALID_TXN")).thenReturn(Optional.empty());
|
||||
|
||||
assertThatThrownBy(() -> acquisitionService.approveAcquisition("INVALID_TXN"))
|
||||
.isInstanceOf(AcquisitionNotFoundException.class)
|
||||
.hasMessageContaining("INVALID_TXN");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("매입 승인 실패 - 이미 승인된 매입")
|
||||
void approveAcquisition_AlreadyApproved() {
|
||||
pendingAcquisition.setStatus(AcquisitionStatus.APPROVED);
|
||||
when(acquisitionRepository.findByTransactionId("TXN123456789")).thenReturn(Optional.of(pendingAcquisition));
|
||||
|
||||
assertThatThrownBy(() -> acquisitionService.approveAcquisition("TXN123456789"))
|
||||
.isInstanceOf(InvalidAcquisitionStateException.class)
|
||||
.hasMessageContaining("APPROVED");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("매입 거절 처리 성공")
|
||||
void rejectAcquisition_Success() {
|
||||
when(acquisitionRepository.findByTransactionId("TXN123456789")).thenReturn(Optional.of(pendingAcquisition));
|
||||
when(acquisitionRepository.save(any(Acquisition.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
AcquisitionResponse response = acquisitionService.rejectAcquisition("TXN123456789", "한도 초과");
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatus()).isEqualTo(AcquisitionStatus.REJECTED);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("매입 정보 조회 성공")
|
||||
void getAcquisition_Success() {
|
||||
when(acquisitionRepository.findByTransactionId("TXN123456789")).thenReturn(Optional.of(pendingAcquisition));
|
||||
|
||||
AcquisitionResponse response = acquisitionService.getAcquisition("TXN123456789");
|
||||
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getTransactionId()).isEqualTo("TXN123456789");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("가맹점별 매입 목록 조회 성공")
|
||||
void getAcquisitionsByMerchant_Success() {
|
||||
Acquisition acquisition2 = Acquisition.builder()
|
||||
.id(2L).merchantId("MERCHANT001").cardNumber("987654****3210")
|
||||
.amount(new BigDecimal("30000.00")).transactionId("TXN987654321")
|
||||
.status(AcquisitionStatus.PENDING).createdAt(LocalDateTime.now()).build();
|
||||
|
||||
when(acquisitionRepository.findByMerchantId("MERCHANT001")).thenReturn(Arrays.asList(pendingAcquisition, acquisition2));
|
||||
|
||||
List<AcquisitionResponse> responses = acquisitionService.getAcquisitionsByMerchant("MERCHANT001");
|
||||
|
||||
assertThat(responses).hasSize(2);
|
||||
assertThat(responses).extracting(AcquisitionResponse::getMerchantId).containsOnly("MERCHANT001");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("대기 중인 매입 목록 조회 성공")
|
||||
void getPendingAcquisitions_Success() {
|
||||
when(acquisitionRepository.findByStatus(AcquisitionStatus.PENDING)).thenReturn(Arrays.asList(pendingAcquisition));
|
||||
|
||||
List<AcquisitionResponse> responses = acquisitionService.getPendingAcquisitions();
|
||||
|
||||
assertThat(responses).hasSize(1);
|
||||
assertThat(responses.get(0).getStatus()).isEqualTo(AcquisitionStatus.PENDING);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue