카드 매입 승인 경로 Spring Boot 3 전환 #1
8 changed files with 656 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>proj-acquirex</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<name>proj-acquirex</name>
|
||||
<description>Card Acquisition Approval System - 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>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
12
src/main/java/com/acquirex/AcquirexApplication.java
Normal file
12
src/main/java/com/acquirex/AcquirexApplication.java
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package com.acquirex;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class AcquirexApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AcquirexApplication.class, args);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
package com.acquirex.controller;
|
||||
|
||||
import com.acquirex.dto.ApprovalRequest;
|
||||
import com.acquirex.dto.ApprovalResponse;
|
||||
import com.acquirex.exception.ApprovalProcessingException;
|
||||
import com.acquirex.exception.GlobalExceptionHandler;
|
||||
import com.acquirex.service.AcquisitionApprovalService;
|
||||
import jakarta.validation.Valid;
|
||||
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.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/acquisitions")
|
||||
public class AcquisitionApprovalController {
|
||||
|
||||
private final AcquisitionApprovalService service;
|
||||
|
||||
public AcquisitionApprovalController(AcquisitionApprovalService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<ApprovalResponse> requestApproval(@Valid @RequestBody ApprovalRequest request) {
|
||||
ApprovalResponse response = service.requestApproval(request);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/approve")
|
||||
public ResponseEntity<ApprovalResponse> approve(@PathVariable Long id) {
|
||||
ApprovalResponse response = service.processApproval(id, true, null);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reject")
|
||||
public ResponseEntity<ApprovalResponse> reject(@PathVariable Long id, @RequestParam String reason) {
|
||||
ApprovalResponse response = service.processApproval(id, false, reason);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<ApprovalResponse> getApproval(@PathVariable Long id) {
|
||||
ApprovalResponse response = service.getApproval(id);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@GetMapping("/pending")
|
||||
public ResponseEntity<List<ApprovalResponse>> getPendingApprovals() {
|
||||
List<ApprovalResponse> responses = service.getPendingApprovals();
|
||||
return ResponseEntity.ok(responses);
|
||||
}
|
||||
|
||||
@GetMapping("/merchant/{merchantId}")
|
||||
public ResponseEntity<List<ApprovalResponse>> getApprovalsByMerchant(@PathVariable String merchantId) {
|
||||
List<ApprovalResponse> responses = service.getApprovalsByMerchant(merchantId);
|
||||
return ResponseEntity.ok(responses);
|
||||
}
|
||||
}
|
||||
|
||||
// DTOs
|
||||
class ApprovalRequest {
|
||||
@jakarta.validation.constraints.NotBlank(message = "Merchant ID is required")
|
||||
private String merchantId;
|
||||
|
||||
@jakarta.validation.constraints.NotBlank(message = "Card number is required")
|
||||
@jakarta.validation.constraints.Size(min = 13, max = 19, message = "Card number must be between 13 and 19 digits")
|
||||
private String cardNumber;
|
||||
|
||||
@jakarta.validation.constraints.NotNull(message = "Amount is required")
|
||||
@jakarta.validation.constraints.DecimalMin(value = "0.01", message = "Amount must be greater than zero")
|
||||
private BigDecimal amount;
|
||||
|
||||
@jakarta.validation.constraints.NotBlank(message = "Currency is required")
|
||||
@jakarta.validation.constraints.Size(min = 3, max = 3, message = "Currency must be a 3-letter code")
|
||||
private String currency;
|
||||
|
||||
public ApprovalRequest() {}
|
||||
|
||||
public ApprovalRequest(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 ApprovalResponse {
|
||||
private Long id;
|
||||
private String merchantId;
|
||||
private String cardNumber;
|
||||
private BigDecimal amount;
|
||||
private String currency;
|
||||
private String status;
|
||||
private String approvalCode;
|
||||
private String rejectionReason;
|
||||
private LocalDateTime requestedAt;
|
||||
private LocalDateTime processedAt;
|
||||
|
||||
public ApprovalResponse() {}
|
||||
|
||||
public ApprovalResponse(Long id, String merchantId, String cardNumber, BigDecimal amount,
|
||||
String currency, String status, String approvalCode,
|
||||
String rejectionReason, LocalDateTime requestedAt, LocalDateTime processedAt) {
|
||||
this.id = id;
|
||||
this.merchantId = merchantId;
|
||||
this.cardNumber = cardNumber;
|
||||
this.amount = amount;
|
||||
this.currency = currency;
|
||||
this.status = status;
|
||||
this.approvalCode = approvalCode;
|
||||
this.rejectionReason = rejectionReason;
|
||||
this.requestedAt = requestedAt;
|
||||
this.processedAt = processedAt;
|
||||
}
|
||||
|
||||
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 String getStatus() { return status; }
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
public String getApprovalCode() { return approvalCode; }
|
||||
public void setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; }
|
||||
public String getRejectionReason() { return rejectionReason; }
|
||||
public void setRejectionReason(String rejectionReason) { this.rejectionReason = rejectionReason; }
|
||||
public LocalDateTime getRequestedAt() { return requestedAt; }
|
||||
public void setRequestedAt(LocalDateTime requestedAt) { this.requestedAt = requestedAt; }
|
||||
public LocalDateTime getProcessedAt() { return processedAt; }
|
||||
public void setProcessedAt(LocalDateTime processedAt) { this.processedAt = processedAt; }
|
||||
}
|
||||
|
||||
// Exceptions
|
||||
class ApprovalProcessingException extends RuntimeException {
|
||||
public ApprovalProcessingException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
@RestControllerAdvice
|
||||
class GlobalExceptionHandler {
|
||||
|
||||
@org.springframework.web.bind.annotation.ExceptionHandler(ApprovalProcessingException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleApprovalProcessingException(ApprovalProcessingException ex) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("timestamp", LocalDateTime.now());
|
||||
body.put("status", HttpStatus.BAD_REQUEST.value());
|
||||
body.put("error", "Approval Processing Error");
|
||||
body.put("message", ex.getMessage());
|
||||
return ResponseEntity.badRequest().body(body);
|
||||
}
|
||||
|
||||
@org.springframework.web.bind.annotation.ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleValidationExceptions(MethodArgumentNotValidException ex) {
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("timestamp", LocalDateTime.now());
|
||||
body.put("status", HttpStatus.BAD_REQUEST.value());
|
||||
body.put("error", "Validation Error");
|
||||
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
ex.getBindingResult().getAllErrors().forEach(error -> {
|
||||
String fieldName = ((FieldError) error).getField();
|
||||
String errorMessage = error.getDefaultMessage();
|
||||
errors.put(fieldName, errorMessage);
|
||||
});
|
||||
body.put("errors", errors);
|
||||
return ResponseEntity.badRequest().body(body);
|
||||
}
|
||||
}
|
||||
72
src/main/java/com/acquirex/domain/AcquisitionApproval.java
Normal file
72
src/main/java/com/acquirex/domain/AcquisitionApproval.java
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package com.acquirex.domain;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "acquisition_approvals")
|
||||
public class AcquisitionApproval {
|
||||
|
||||
@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 ApprovalStatus status;
|
||||
|
||||
@Column(name = "approval_code")
|
||||
private String approvalCode;
|
||||
|
||||
@Column(name = "rejection_reason")
|
||||
private String rejectionReason;
|
||||
|
||||
@Column(name = "requested_at", nullable = false)
|
||||
private LocalDateTime requestedAt;
|
||||
|
||||
@Column(name = "processed_at")
|
||||
private LocalDateTime processedAt;
|
||||
|
||||
public enum ApprovalStatus {
|
||||
PENDING, APPROVED, REJECTED, CANCELLED
|
||||
}
|
||||
|
||||
public AcquisitionApproval() {
|
||||
this.requestedAt = LocalDateTime.now();
|
||||
this.status = ApprovalStatus.PENDING;
|
||||
}
|
||||
|
||||
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 ApprovalStatus getStatus() { return status; }
|
||||
public void setStatus(ApprovalStatus status) { this.status = status; }
|
||||
public String getApprovalCode() { return approvalCode; }
|
||||
public void setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; }
|
||||
public String getRejectionReason() { return rejectionReason; }
|
||||
public void setRejectionReason(String rejectionReason) { this.rejectionReason = rejectionReason; }
|
||||
public LocalDateTime getRequestedAt() { return requestedAt; }
|
||||
public void setRequestedAt(LocalDateTime requestedAt) { this.requestedAt = requestedAt; }
|
||||
public LocalDateTime getProcessedAt() { return processedAt; }
|
||||
public void setProcessedAt(LocalDateTime processedAt) { this.processedAt = processedAt; }
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package com.acquirex.repository;
|
||||
|
||||
import com.acquirex.domain.AcquisitionApproval;
|
||||
import com.acquirex.domain.AcquisitionApproval.ApprovalStatus;
|
||||
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 AcquisitionApprovalRepository extends JpaRepository<AcquisitionApproval, Long> {
|
||||
|
||||
List<AcquisitionApproval> findByMerchantId(String merchantId);
|
||||
|
||||
List<AcquisitionApproval> findByStatus(ApprovalStatus status);
|
||||
|
||||
Optional<AcquisitionApproval> findByApprovalCode(String approvalCode);
|
||||
|
||||
List<AcquisitionApproval> findByMerchantIdAndStatus(String merchantId, ApprovalStatus status);
|
||||
|
||||
List<AcquisitionApproval> findByRequestedAtBetween(LocalDateTime start, LocalDateTime end);
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package com.acquirex.service;
|
||||
|
||||
import com.acquirex.domain.AcquisitionApproval;
|
||||
import com.acquirex.domain.AcquisitionApproval.ApprovalStatus;
|
||||
import com.acquirex.dto.ApprovalRequest;
|
||||
import com.acquirex.dto.ApprovalResponse;
|
||||
import com.acquirex.exception.ApprovalProcessingException;
|
||||
import com.acquirex.repository.AcquisitionApprovalRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class AcquisitionApprovalService {
|
||||
|
||||
private final AcquisitionApprovalRepository repository;
|
||||
|
||||
public AcquisitionApprovalService(AcquisitionApprovalRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ApprovalResponse requestApproval(ApprovalRequest request) {
|
||||
validateRequest(request);
|
||||
|
||||
AcquisitionApproval approval = new AcquisitionApproval();
|
||||
approval.setMerchantId(request.getMerchantId());
|
||||
approval.setCardNumber(maskCardNumber(request.getCardNumber()));
|
||||
approval.setAmount(request.getAmount());
|
||||
approval.setCurrency(request.getCurrency());
|
||||
|
||||
AcquisitionApproval saved = repository.save(approval);
|
||||
return toResponse(saved);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ApprovalResponse processApproval(Long id, boolean approved, String reason) {
|
||||
AcquisitionApproval approval = repository.findById(id)
|
||||
.orElseThrow(() -> new ApprovalProcessingException("Approval not found: " + id));
|
||||
|
||||
if (approval.getStatus() != ApprovalStatus.PENDING) {
|
||||
throw new ApprovalProcessingException("Approval already processed");
|
||||
}
|
||||
|
||||
if (approved) {
|
||||
approval.setStatus(ApprovalStatus.APPROVED);
|
||||
approval.setApprovalCode(generateApprovalCode());
|
||||
} else {
|
||||
approval.setStatus(ApprovalStatus.REJECTED);
|
||||
approval.setRejectionReason(reason);
|
||||
}
|
||||
approval.setProcessedAt(LocalDateTime.now());
|
||||
|
||||
AcquisitionApproval saved = repository.save(approval);
|
||||
return toResponse(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ApprovalResponse getApproval(Long id) {
|
||||
return repository.findById(id)
|
||||
.map(this::toResponse)
|
||||
.orElseThrow(() -> new ApprovalProcessingException("Approval not found: " + id));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ApprovalResponse> getPendingApprovals() {
|
||||
return repository.findByStatus(ApprovalStatus.PENDING)
|
||||
.stream()
|
||||
.map(this::toResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ApprovalResponse> getApprovalsByMerchant(String merchantId) {
|
||||
return repository.findByMerchantId(merchantId)
|
||||
.stream()
|
||||
.map(this::toResponse)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void validateRequest(ApprovalRequest request) {
|
||||
if (request.getMerchantId() == null || request.getMerchantId().isBlank()) {
|
||||
throw new ApprovalProcessingException("Merchant ID is required");
|
||||
}
|
||||
if (request.getCardNumber() == null || request.getCardNumber().length() < 13) {
|
||||
throw new ApprovalProcessingException("Valid card number is required");
|
||||
}
|
||||
if (request.getAmount() == null || request.getAmount().compareTo(java.math.BigDecimal.ZERO) <= 0) {
|
||||
throw new ApprovalProcessingException("Amount must be positive");
|
||||
}
|
||||
if (request.getCurrency() == null || request.getCurrency().length() != 3) {
|
||||
throw new ApprovalProcessingException("Valid currency code is required");
|
||||
}
|
||||
}
|
||||
|
||||
private String maskCardNumber(String cardNumber) {
|
||||
if (cardNumber == null || cardNumber.length() < 4) {
|
||||
return "****";
|
||||
}
|
||||
return "****-****-****-" + cardNumber.substring(cardNumber.length() - 4);
|
||||
}
|
||||
|
||||
private String generateApprovalCode() {
|
||||
return "APP-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||
}
|
||||
|
||||
private ApprovalResponse toResponse(AcquisitionApproval approval) {
|
||||
return new ApprovalResponse(
|
||||
approval.getId(),
|
||||
approval.getMerchantId(),
|
||||
approval.getCardNumber(),
|
||||
approval.getAmount(),
|
||||
approval.getCurrency(),
|
||||
approval.getStatus().name(),
|
||||
approval.getApprovalCode(),
|
||||
approval.getRejectionReason(),
|
||||
approval.getRequestedAt(),
|
||||
approval.getProcessedAt()
|
||||
);
|
||||
}
|
||||
}
|
||||
20
src/main/resources/application.properties
Normal file
20
src/main/resources/application.properties
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
spring.application.name=proj-acquirex
|
||||
|
||||
# H2 Database Configuration
|
||||
spring.datasource.url=jdbc:h2:mem:acquirexdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
|
||||
# JPA/Hibernate Configuration
|
||||
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 Configuration
|
||||
server.port=8080
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
package com.acquirex.service;
|
||||
|
||||
import com.acquirex.domain.AcquisitionApproval;
|
||||
import com.acquirex.domain.AcquisitionApproval.ApprovalStatus;
|
||||
import com.acquirex.dto.ApprovalRequest;
|
||||
import com.acquirex.dto.ApprovalResponse;
|
||||
import com.acquirex.exception.ApprovalProcessingException;
|
||||
import com.acquirex.repository.AcquisitionApprovalRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
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 AcquisitionApprovalServiceTest {
|
||||
|
||||
@Mock
|
||||
private AcquisitionApprovalRepository repository;
|
||||
|
||||
@InjectMocks
|
||||
private AcquisitionApprovalService service;
|
||||
|
||||
private ApprovalRequest validRequest;
|
||||
private AcquisitionApproval savedApproval;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
validRequest = new ApprovalRequest("MERCHANT001", "4111111111111111", new BigDecimal("100.00"), "KRW");
|
||||
|
||||
savedApproval = new AcquisitionApproval();
|
||||
savedApproval.setId(1L);
|
||||
savedApproval.setMerchantId("MERCHANT001");
|
||||
savedApproval.setCardNumber("****-****-****-1111");
|
||||
savedApproval.setAmount(new BigDecimal("100.00"));
|
||||
savedApproval.setCurrency("KRW");
|
||||
savedApproval.setStatus(ApprovalStatus.PENDING);
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestApproval_ValidRequest_ReturnsApprovalResponse() {
|
||||
when(repository.save(any(AcquisitionApproval.class))).thenReturn(savedApproval);
|
||||
|
||||
ApprovalResponse response = service.requestApproval(validRequest);
|
||||
|
||||
assertNotNull(response);
|
||||
assertEquals(1L, response.getId());
|
||||
assertEquals("MERCHANT001", response.getMerchantId());
|
||||
assertEquals("PENDING", response.getStatus());
|
||||
assertTrue(response.getCardNumber().startsWith("****"));
|
||||
verify(repository, times(1)).save(any(AcquisitionApproval.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestApproval_InvalidMerchantId_ThrowsException() {
|
||||
validRequest.setMerchantId("");
|
||||
assertThrows(ApprovalProcessingException.class, () -> service.requestApproval(validRequest));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestApproval_InvalidAmount_ThrowsException() {
|
||||
validRequest.setAmount(BigDecimal.ZERO);
|
||||
assertThrows(ApprovalProcessingException.class, () -> service.requestApproval(validRequest));
|
||||
}
|
||||
|
||||
@Test
|
||||
void processApproval_Approve_UpdatesStatusToApproved() {
|
||||
savedApproval.setStatus(ApprovalStatus.PENDING);
|
||||
when(repository.findById(1L)).thenReturn(Optional.of(savedApproval));
|
||||
when(repository.save(any(AcquisitionApproval.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
ApprovalResponse response = service.processApproval(1L, true, null);
|
||||
|
||||
assertEquals("APPROVED", response.getStatus());
|
||||
assertNotNull(response.getApprovalCode());
|
||||
assertTrue(response.getApprovalCode().startsWith("APP-"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void processApproval_Reject_UpdatesStatusToRejected() {
|
||||
savedApproval.setStatus(ApprovalStatus.PENDING);
|
||||
when(repository.findById(1L)).thenReturn(Optional.of(savedApproval));
|
||||
when(repository.save(any(AcquisitionApproval.class))).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
ApprovalResponse response = service.processApproval(1L, false, "Insufficient funds");
|
||||
|
||||
assertEquals("REJECTED", response.getStatus());
|
||||
assertEquals("Insufficient funds", response.getRejectionReason());
|
||||
assertNull(response.getApprovalCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void processApproval_AlreadyProcessed_ThrowsException() {
|
||||
savedApproval.setStatus(ApprovalStatus.APPROVED);
|
||||
when(repository.findById(1L)).thenReturn(Optional.of(savedApproval));
|
||||
assertThrows(ApprovalProcessingException.class, () -> service.processApproval(1L, true, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void processApproval_NotFound_ThrowsException() {
|
||||
when(repository.findById(999L)).thenReturn(Optional.empty());
|
||||
assertThrows(ApprovalProcessingException.class, () -> service.processApproval(999L, true, null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getApproval_ExistingId_ReturnsApproval() {
|
||||
when(repository.findById(1L)).thenReturn(Optional.of(savedApproval));
|
||||
ApprovalResponse response = service.getApproval(1L);
|
||||
assertNotNull(response);
|
||||
assertEquals(1L, response.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getApproval_NonExistingId_ThrowsException() {
|
||||
when(repository.findById(999L)).thenReturn(Optional.empty());
|
||||
assertThrows(ApprovalProcessingException.class, () -> service.getApproval(999L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPendingApprovals_ReturnsPendingList() {
|
||||
AcquisitionApproval pending1 = new AcquisitionApproval();
|
||||
pending1.setId(1L);
|
||||
pending1.setStatus(ApprovalStatus.PENDING);
|
||||
pending1.setMerchantId("MERCHANT001");
|
||||
|
||||
AcquisitionApproval pending2 = new AcquisitionApproval();
|
||||
pending2.setId(2L);
|
||||
pending2.setStatus(ApprovalStatus.PENDING);
|
||||
pending2.setMerchantId("MERCHANT002");
|
||||
|
||||
when(repository.findByStatus(ApprovalStatus.PENDING)).thenReturn(Arrays.asList(pending1, pending2));
|
||||
|
||||
List<ApprovalResponse> responses = service.getPendingApprovals();
|
||||
assertEquals(2, responses.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getApprovalsByMerchant_ReturnsMerchantApprovals() {
|
||||
when(repository.findByMerchantId("MERCHANT001")).thenReturn(Arrays.asList(savedApproval));
|
||||
|
||||
List<ApprovalResponse> responses = service.getApprovalsByMerchant("MERCHANT001");
|
||||
assertEquals(1, responses.size());
|
||||
assertEquals("MERCHANT001", responses.get(0).getMerchantId());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue