카드 매입 승인 경로 Spring Boot 3 전환 #11
8 changed files with 594 additions and 0 deletions
57
pom.xml
Normal file
57
pom.xml
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
<?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 https://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>card-acquisition</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>card-acquisition</name>
|
||||
<description>카드 매입 승인 경로 - Spring Boot 3 전환</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</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-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</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>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.acquirex.cardacquisition;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class CardAcquisitionApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(CardAcquisitionApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
package com.acquirex.cardacquisition.controller;
|
||||
|
||||
import com.acquirex.cardacquisition.dto.AcquisitionRequest;
|
||||
import com.acquirex.cardacquisition.dto.AcquisitionResponse;
|
||||
import com.acquirex.cardacquisition.service.CardAcquisitionService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** Spring Boot 3 전환: javax.servlet → jakarta.servlet */
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/acquisitions")
|
||||
public class CardAcquisitionController {
|
||||
|
||||
private final CardAcquisitionService service;
|
||||
|
||||
public CardAcquisitionController(CardAcquisitionService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<AcquisitionResponse> requestAcquisition(@Valid @RequestBody AcquisitionRequest request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(service.requestAcquisition(request));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<AcquisitionResponse> getAcquisition(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(service.getAcquisition(id));
|
||||
}
|
||||
|
||||
@GetMapping("/merchant/{merchantId}")
|
||||
public ResponseEntity<List<AcquisitionResponse>> getAcquisitionsByMerchant(@PathVariable String merchantId) {
|
||||
return ResponseEntity.ok(service.getAcquisitionsByMerchant(merchantId));
|
||||
}
|
||||
|
||||
@GetMapping("/pending")
|
||||
public ResponseEntity<List<AcquisitionResponse>> getPendingAcquisitions() {
|
||||
return ResponseEntity.ok(service.getPendingAcquisitions());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/cancel")
|
||||
public ResponseEntity<AcquisitionResponse> cancelAcquisition(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(service.cancelAcquisition(id));
|
||||
}
|
||||
}
|
||||
|
||||
/** Spring Boot 3 전환: javax.validation → jakarta.validation */
|
||||
@RestControllerAdvice
|
||||
class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(CardAcquisitionService.AcquisitionNotFoundException.class)
|
||||
public ResponseEntity<java.util.Map<String, Object>> handleNotFound(CardAcquisitionService.AcquisitionNotFoundException ex) {
|
||||
return buildErrorResponse(HttpStatus.NOT_FOUND, ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(CardAcquisitionService.InvalidAcquisitionException.class)
|
||||
public ResponseEntity<java.util.Map<String, Object>> handleInvalid(CardAcquisitionService.InvalidAcquisitionException ex) {
|
||||
return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalStateException.class)
|
||||
public ResponseEntity<java.util.Map<String, Object>> handleIllegalState(IllegalStateException ex) {
|
||||
return buildErrorResponse(HttpStatus.CONFLICT, ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<java.util.Map<String, Object>> handleValidationErrors(
|
||||
org.springframework.web.bind.MethodArgumentNotValidException ex) {
|
||||
java.util.Map<String, String> fieldErrors = new java.util.HashMap<>();
|
||||
ex.getBindingResult().getFieldErrors().forEach(e -> fieldErrors.put(e.getField(), e.getDefaultMessage()));
|
||||
|
||||
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
||||
body.put("timestamp", LocalDateTime.now());
|
||||
body.put("status", HttpStatus.BAD_REQUEST.value());
|
||||
body.put("error", "Validation Failed");
|
||||
body.put("fieldErrors", fieldErrors);
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body);
|
||||
}
|
||||
|
||||
private ResponseEntity<java.util.Map<String, Object>> buildErrorResponse(HttpStatus status, String message) {
|
||||
java.util.Map<String, Object> body = new java.util.HashMap<>();
|
||||
body.put("timestamp", LocalDateTime.now());
|
||||
body.put("status", status.value());
|
||||
body.put("error", status.getReasonPhrase());
|
||||
body.put("message", message);
|
||||
return ResponseEntity.status(status).body(body);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
package com.acquirex.cardacquisition.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** Spring Boot 3 전환: javax.persistence → jakarta.persistence */
|
||||
@Entity
|
||||
@Table(name = "card_acquisitions")
|
||||
public class CardAcquisition {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "merchant_id", nullable = false)
|
||||
private String merchantId;
|
||||
|
||||
@Column(name = "card_number", nullable = false)
|
||||
private String cardNumber;
|
||||
|
||||
@Column(name = "amount", nullable = false, precision = 15, scale = 2)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Column(name = "currency", nullable = false, length = 3)
|
||||
private String currency;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false)
|
||||
private AcquisitionStatus status = AcquisitionStatus.PENDING;
|
||||
|
||||
@Column(name = "approval_code")
|
||||
private String approvalCode;
|
||||
|
||||
@Column(name = "request_time", nullable = false)
|
||||
private LocalDateTime requestTime = LocalDateTime.now();
|
||||
|
||||
@Column(name = "approval_time")
|
||||
private LocalDateTime approvalTime;
|
||||
|
||||
@Column(name = "reject_reason")
|
||||
private String rejectReason;
|
||||
|
||||
public CardAcquisition() {}
|
||||
|
||||
public CardAcquisition(String merchantId, String cardNumber, BigDecimal amount, String currency) {
|
||||
this.merchantId = merchantId;
|
||||
this.cardNumber = cardNumber;
|
||||
this.amount = amount;
|
||||
this.currency = currency;
|
||||
this.status = AcquisitionStatus.PENDING;
|
||||
this.requestTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getMerchantId() { return merchantId; }
|
||||
public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
|
||||
public String getCardNumber() { return cardNumber; }
|
||||
public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
public String getCurrency() { return currency; }
|
||||
public void setCurrency(String currency) { this.currency = currency; }
|
||||
public AcquisitionStatus getStatus() { return status; }
|
||||
public void setStatus(AcquisitionStatus status) { this.status = status; }
|
||||
public String getApprovalCode() { return approvalCode; }
|
||||
public void setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; }
|
||||
public LocalDateTime getRequestTime() { return requestTime; }
|
||||
public void setRequestTime(LocalDateTime requestTime) { this.requestTime = requestTime; }
|
||||
public LocalDateTime getApprovalTime() { return approvalTime; }
|
||||
public void setApprovalTime(LocalDateTime approvalTime) { this.approvalTime = approvalTime; }
|
||||
public String getRejectReason() { return rejectReason; }
|
||||
public void setRejectReason(String rejectReason) { this.rejectReason = rejectReason; }
|
||||
|
||||
public void approve(String approvalCode) {
|
||||
this.status = AcquisitionStatus.APPROVED;
|
||||
this.approvalCode = approvalCode;
|
||||
this.approvalTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void reject(String reason) {
|
||||
this.status = AcquisitionStatus.REJECTED;
|
||||
this.rejectReason = reason;
|
||||
this.approvalTime = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/** Spring Boot 3 전환: javax.persistence → jakarta.persistence */
|
||||
public enum AcquisitionStatus {
|
||||
PENDING, APPROVED, REJECTED, CANCELLED
|
||||
}
|
||||
}
|
||||
|
||||
/** Spring Boot 3 전환: javax.validation → jakarta.validation */
|
||||
class AcquisitionRequest {
|
||||
@NotBlank(message = "상점 ID는 필수입니다")
|
||||
private String merchantId;
|
||||
|
||||
@NotBlank(message = "카드번호는 필수입니다")
|
||||
@Pattern(regexp = "\\d{10,19}", message = "유효하지 않은 카드번호 형식입니다")
|
||||
private String cardNumber;
|
||||
|
||||
@NotNull(message = "금액은 필수입니다")
|
||||
@DecimalMin(value = "0.01", message = "금액은 0보다 커야 합니다")
|
||||
private BigDecimal amount;
|
||||
|
||||
@NotBlank(message = "통화는 필수입니다")
|
||||
@Size(min = 3, max = 3, message = "통화 코드는 3자리입니다")
|
||||
private String currency;
|
||||
|
||||
public AcquisitionRequest() {}
|
||||
public AcquisitionRequest(String merchantId, String cardNumber, BigDecimal amount, String currency) {
|
||||
this.merchantId = merchantId;
|
||||
this.cardNumber = cardNumber;
|
||||
this.amount = amount;
|
||||
this.currency = currency;
|
||||
}
|
||||
|
||||
public String getMerchantId() { return merchantId; }
|
||||
public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
|
||||
public String getCardNumber() { return cardNumber; }
|
||||
public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
public String getCurrency() { return currency; }
|
||||
public void setCurrency(String currency) { this.currency = currency; }
|
||||
}
|
||||
|
||||
class AcquisitionResponse {
|
||||
private Long id;
|
||||
private String merchantId;
|
||||
private String maskedCardNumber;
|
||||
private BigDecimal amount;
|
||||
private String currency;
|
||||
private CardAcquisition.AcquisitionStatus status;
|
||||
private String approvalCode;
|
||||
private LocalDateTime requestTime;
|
||||
private LocalDateTime approvalTime;
|
||||
private String message;
|
||||
|
||||
public AcquisitionResponse() {}
|
||||
|
||||
public static AcquisitionResponse fromEntity(CardAcquisition entity) {
|
||||
AcquisitionResponse response = new AcquisitionResponse();
|
||||
response.setId(entity.getId());
|
||||
response.setMerchantId(entity.getMerchantId());
|
||||
response.setMaskedCardNumber(maskCardNumber(entity.getCardNumber()));
|
||||
response.setAmount(entity.getAmount());
|
||||
response.setCurrency(entity.getCurrency());
|
||||
response.setStatus(entity.getStatus());
|
||||
response.setApprovalCode(entity.getApprovalCode());
|
||||
response.setRequestTime(entity.getRequestTime());
|
||||
response.setApprovalTime(entity.getApprovalTime());
|
||||
return response;
|
||||
}
|
||||
|
||||
private static String maskCardNumber(String cardNumber) {
|
||||
if (cardNumber == null || cardNumber.length() < 8) return "****";
|
||||
return cardNumber.substring(0, 4) + "****" + cardNumber.substring(cardNumber.length() - 4);
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getMerchantId() { return merchantId; }
|
||||
public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
|
||||
public String getMaskedCardNumber() { return maskedCardNumber; }
|
||||
public void setMaskedCardNumber(String maskedCardNumber) { this.maskedCardNumber = maskedCardNumber; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
public String getCurrency() { return currency; }
|
||||
public void setCurrency(String currency) { this.currency = currency; }
|
||||
public CardAcquisition.AcquisitionStatus getStatus() { return status; }
|
||||
public void setStatus(CardAcquisition.AcquisitionStatus status) { this.status = status; }
|
||||
public String getApprovalCode() { return approvalCode; }
|
||||
public void setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; }
|
||||
public LocalDateTime getRequestTime() { return requestTime; }
|
||||
public void setRequestTime(LocalDateTime requestTime) { this.requestTime = requestTime; }
|
||||
public LocalDateTime getApprovalTime() { return approvalTime; }
|
||||
public void setApprovalTime(LocalDateTime approvalTime) { this.approvalTime = approvalTime; }
|
||||
public String getMessage() { return message; }
|
||||
public void setMessage(String message) { this.message = message; }
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package com.acquirex.cardacquisition.repository;
|
||||
|
||||
import com.acquirex.cardacquisition.entity.CardAcquisition;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface CardAcquisitionRepository extends JpaRepository<CardAcquisition, Long> {
|
||||
|
||||
List<CardAcquisition> findByMerchantId(String merchantId);
|
||||
|
||||
List<CardAcquisition> findByStatus(CardAcquisition.AcquisitionStatus status);
|
||||
|
||||
Optional<CardAcquisition> findByApprovalCode(String approvalCode);
|
||||
|
||||
List<CardAcquisition> findByRequestTimeBetween(LocalDateTime start, LocalDateTime end);
|
||||
|
||||
List<CardAcquisition> findByMerchantIdAndStatus(String merchantId, CardAcquisition.AcquisitionStatus status);
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package com.acquirex.cardacquisition.service;
|
||||
|
||||
import com.acquirex.cardacquisition.entity.CardAcquisition;
|
||||
import com.acquirex.cardacquisition.repository.CardAcquisitionRepository;
|
||||
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.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class CardAcquisitionService {
|
||||
|
||||
private static final BigDecimal MAX_SINGLE_TRANSACTION = new BigDecimal("10000000");
|
||||
private static final BigDecimal DAILY_LIMIT = new BigDecimal("50000000");
|
||||
|
||||
private final CardAcquisitionRepository repository;
|
||||
|
||||
public CardAcquisitionService(CardAcquisitionRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AcquisitionResponse requestAcquisition(AcquisitionRequest request) {
|
||||
validateRequest(request);
|
||||
|
||||
CardAcquisition acquisition = new CardAcquisition(
|
||||
request.getMerchantId(), request.getCardNumber(), request.getAmount(), request.getCurrency());
|
||||
|
||||
CardAcquisition saved = repository.save(acquisition);
|
||||
String approvalCode = generateApprovalCode();
|
||||
saved.approve(approvalCode);
|
||||
|
||||
AcquisitionResponse response = AcquisitionResponse.fromEntity(repository.save(saved));
|
||||
response.setMessage("매입 승인이 완료되었습니다");
|
||||
return response;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public AcquisitionResponse getAcquisition(Long id) {
|
||||
CardAcquisition acquisition = repository.findById(id)
|
||||
.orElseThrow(() -> new AcquisitionNotFoundException("매입 정보를 찾을 수 없습니다: " + id));
|
||||
AcquisitionResponse response = AcquisitionResponse.fromEntity(acquisition);
|
||||
response.setMessage("매입 정보를 조회했습니다");
|
||||
return response;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<AcquisitionResponse> getAcquisitionsByMerchant(String merchantId) {
|
||||
return repository.findByMerchantId(merchantId).stream()
|
||||
.map(AcquisitionResponse::fromEntity).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<AcquisitionResponse> getPendingAcquisitions() {
|
||||
return repository.findByStatus(CardAcquisition.AcquisitionStatus.PENDING).stream()
|
||||
.map(AcquisitionResponse::fromEntity).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AcquisitionResponse cancelAcquisition(Long id) {
|
||||
CardAcquisition acquisition = repository.findById(id)
|
||||
.orElseThrow(() -> new AcquisitionNotFoundException("매입 정보를 찾을 수 없습니다: " + id));
|
||||
|
||||
if (acquisition.getStatus() == CardAcquisition.AcquisitionStatus.CANCELLED) {
|
||||
throw new IllegalStateException("이미 취소된 매입입니다");
|
||||
}
|
||||
|
||||
acquisition.setStatus(CardAcquisition.AcquisitionStatus.CANCELLED);
|
||||
acquisition.setApprovalTime(LocalDateTime.now());
|
||||
|
||||
AcquisitionResponse response = AcquisitionResponse.fromEntity(repository.save(acquisition));
|
||||
response.setMessage("매입이 취소되었습니다");
|
||||
return response;
|
||||
}
|
||||
|
||||
private void validateRequest(AcquisitionRequest request) {
|
||||
if (request.getAmount().compareTo(MAX_SINGLE_TRANSACTION) > 0) {
|
||||
throw new InvalidAcquisitionException("단일 거래 한도를 초과했습니다");
|
||||
}
|
||||
|
||||
LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
|
||||
BigDecimal todayTotal = repository.findByRequestTimeBetween(startOfDay, startOfDay.plusDays(1)).stream()
|
||||
.filter(a -> a.getMerchantId().equals(request.getMerchantId()))
|
||||
.map(CardAcquisition::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
if (todayTotal.add(request.getAmount()).compareTo(DAILY_LIMIT) > 0) {
|
||||
throw new InvalidAcquisitionException("일일 한도를 초과했습니다");
|
||||
}
|
||||
}
|
||||
|
||||
private String generateApprovalCode() {
|
||||
return "APR" + UUID.randomUUID().toString().substring(0, 12).toUpperCase();
|
||||
}
|
||||
|
||||
public static class AcquisitionNotFoundException extends RuntimeException {
|
||||
public AcquisitionNotFoundException(String message) { super(message); }
|
||||
}
|
||||
|
||||
public static class InvalidAcquisitionException extends RuntimeException {
|
||||
public InvalidAcquisitionException(String message) { super(message); }
|
||||
}
|
||||
}
|
||||
24
src/main/resources/application.properties
Normal file
24
src/main/resources/application.properties
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
spring.application.name=card-acquisition
|
||||
|
||||
# H2 Database
|
||||
spring.datasource.url=jdbc:h2:mem:cardacquisition;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
|
||||
# JPA/Hibernate 6 (Spring Boot 3)
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
spring.jpa.hibernate.ddl-auto=create-drop
|
||||
spring.jpa.show-sql=true
|
||||
spring.jpa.properties.hibernate.format_sql=true
|
||||
|
||||
# H2 Console
|
||||
spring.h2.console.enabled=true
|
||||
spring.h2.console.path=/h2-console
|
||||
|
||||
# Server
|
||||
server.port=8080
|
||||
|
||||
# Jackson
|
||||
spring.jackson.serialization.write-dates-as-timestamps=false
|
||||
spring.jackson.date-format=yyyy-MM-dd'T'HH:mm:ss
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package com.acquirex.cardacquisition;
|
||||
|
||||
import com.acquirex.cardacquisition.entity.CardAcquisition;
|
||||
import com.acquirex.cardacquisition.repository.CardAcquisitionRepository;
|
||||
import com.acquirex.cardacquisition.service.CardAcquisitionService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CardAcquisitionServiceTest {
|
||||
|
||||
@Mock
|
||||
private CardAcquisitionRepository repository;
|
||||
|
||||
private CardAcquisitionService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CardAcquisitionService(repository);
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestAcquisition_ValidRequest_ReturnsApprovedResponse() {
|
||||
when(repository.save(any(CardAcquisition.class))).thenAnswer(i -> {
|
||||
CardAcquisition e = i.getArgument(0);
|
||||
if (e.getId() == null) e.setId(1L);
|
||||
return e;
|
||||
});
|
||||
when(repository.findByRequestTimeBetween(any(), any())).thenReturn(List.of());
|
||||
|
||||
AcquisitionRequest request = new AcquisitionRequest("MERCHANT001", "1234567890123456", new BigDecimal("50000"), "KRW");
|
||||
AcquisitionResponse response = service.requestAcquisition(request);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(1L, response.getId());
|
||||
assertEquals(CardAcquisition.AcquisitionStatus.APPROVED, response.getStatus());
|
||||
assertTrue(response.getApprovalCode().startsWith("APR"));
|
||||
verify(repository, times(2)).save(any(CardAcquisition.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestAcquisition_ExceedsLimit_ThrowsException() {
|
||||
AcquisitionRequest request = new AcquisitionRequest("MERCHANT001", "1234567890123456", new BigDecimal("15000000"), "KRW");
|
||||
assertThrows(CardAcquisitionService.InvalidAcquisitionException.class, () -> service.requestAcquisition(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAcquisition_ExistingId_ReturnsResponse() {
|
||||
CardAcquisition entity = new CardAcquisition("M1", "1111", new BigDecimal("1000"), "KRW");
|
||||
entity.setId(1L);
|
||||
entity.approve("APR123");
|
||||
when(repository.findById(1L)).thenReturn(Optional.of(entity));
|
||||
|
||||
AcquisitionResponse response = service.getAcquisition(1L);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(CardAcquisition.AcquisitionStatus.APPROVED, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getAcquisition_NotFound_ThrowsException() {
|
||||
when(repository.findById(999L)).thenReturn(Optional.empty());
|
||||
assertThrows(CardAcquisitionService.AcquisitionNotFoundException.class, () -> service.getAcquisition(999L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelAcquisition_ExistingPending_ReturnsCancelled() {
|
||||
CardAcquisition entity = new CardAcquisition("M1", "1111", new BigDecimal("1000"), "KRW");
|
||||
entity.setId(1L);
|
||||
when(repository.findById(1L)).thenReturn(Optional.of(entity));
|
||||
when(repository.save(any())).thenAnswer(i -> i.getArgument(0));
|
||||
|
||||
AcquisitionResponse response = service.cancelAcquisition(1L);
|
||||
|
||||
assertEquals(CardAcquisition.AcquisitionStatus.CANCELLED, response.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelAcquisition_AlreadyCancelled_ThrowsException() {
|
||||
CardAcquisition entity = new CardAcquisition("M1", "1111", new BigDecimal("1000"), "KRW");
|
||||
entity.setId(1L);
|
||||
entity.setStatus(CardAcquisition.AcquisitionStatus.CANCELLED);
|
||||
when(repository.findById(1L)).thenReturn(Optional.of(entity));
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> service.cancelAcquisition(1L));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue