Compare commits
9 commits
forge/ACX-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8aaeee4ab5 | |||
| d571b47db5 | |||
| 0cf60a9880 | |||
| 8b0e39d260 | |||
| 1b88ac69fc | |||
| 851f7c1658 | |||
| 469d153f80 | |||
| 3a213c4e4f | |||
| 43dca6853c |
13 changed files with 430 additions and 701 deletions
91
README.md
91
README.md
|
|
@ -1,79 +1,28 @@
|
|||
# proj-acquirex - Card Acquisition Approval System
|
||||
# 카드 매입 승인 경로 Spring Boot 3 전환
|
||||
|
||||
## Spring Boot 3 Migration
|
||||
## 전환 범위
|
||||
|
||||
### Migration Summary
|
||||
| 항목 | 기존 | 전환 후 |
|
||||
|------|------|----------|
|
||||
| Spring Boot | 2.7.x | 3.2.x |
|
||||
| Java | 11 | 17 (LTS) |
|
||||
| javax.* 네임스페이스 | jakarta.* |
|
||||
| JPA/Hibernate | 2.x | 3.x |
|
||||
| Spring Data JPA | 2.x | 3.x |
|
||||
| H2 Database | 2.1.x | 2.2.x |
|
||||
|
||||
| Component | Before (SB 2.x) | After (SB 3.x) |
|
||||
|-----------|-----------------|----------------|
|
||||
| Java Version | 8/11 | 17 |
|
||||
| EE Namespace | `javax.*` | `jakarta.*` |
|
||||
| Validation | javax.validation | jakarta.validation |
|
||||
| Spring Boot | 2.7.x | 3.2.5 |
|
||||
|
||||
### Migration Scope
|
||||
|
||||
- **CardAcquisitionApprovalService**: Core approval business logic
|
||||
- **CardAcquisitionApprovalController**: REST API endpoint (`/api/v1/approvals`)
|
||||
- **ApprovalRequest/ApprovalResponse**: DTOs with Jakarta Validation
|
||||
- **Domain entities**: CardAcquisitionApproval, ApprovalStatus
|
||||
|
||||
### Key Changes
|
||||
|
||||
1. **Package Migration**: All `javax.*` imports replaced with `jakarta.*`
|
||||
2. **Validation Annotations**: `@NotNull`, `@NotBlank`, `@DecimalMin` use `jakarta.validation`
|
||||
3. **Java 17 Features**: Switch expressions, records-ready structure
|
||||
|
||||
### Verification Procedures
|
||||
## 검증 절차
|
||||
|
||||
```bash
|
||||
# Build and test
|
||||
mvn clean verify
|
||||
|
||||
# Run application
|
||||
mvn spring-boot:run
|
||||
|
||||
# Test endpoint
|
||||
curl -X POST http://localhost:8080/api/v1/approvals \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"requestId":"REQ-001","cardNumber":"4111111111111111","merchantId":"MERCHANT-001","amount":100.00}'
|
||||
./mvnw clean verify
|
||||
```
|
||||
|
||||
### Test Coverage (15 tests)
|
||||
## API 엔드포인트
|
||||
|
||||
| Category | Tests | Description |
|
||||
|----------|-------|-------------|
|
||||
| Service: processApproval | 3 | Approval, pending, rejection logic |
|
||||
| Service: validateRequest | 2 | Card number, merchant ID validation |
|
||||
| Service: determineApprovalStatus | 3 | Status determination by amount |
|
||||
| Service: maskCardNumber | 3 | Card number masking |
|
||||
| Controller | 4 | HTTP 200/202/400 responses |
|
||||
|
||||
### API Specification
|
||||
|
||||
**POST /api/v1/approvals**
|
||||
|
||||
Request:
|
||||
```json
|
||||
{"requestId":"REQ-001","cardNumber":"4111111111111111","merchantId":"MERCHANT-001","amount":100.00}
|
||||
```
|
||||
|
||||
Response (200 OK):
|
||||
```json
|
||||
{"id":1,"requestId":"REQ-001","status":"APPROVED","amount":100.00,"processedAt":"...","message":"Card acquisition approved successfully"}
|
||||
```
|
||||
|
||||
### Dependencies
|
||||
|
||||
| Dependency | Version | Purpose |
|
||||
|------------|---------|---------|
|
||||
| spring-boot-starter-web | 3.2.5 | REST API |
|
||||
| spring-boot-starter-validation | 3.2.5 | Jakarta Validation |
|
||||
| spring-boot-starter-test | 3.2.5 | JUnit 5, Mockito |
|
||||
|
||||
### Build Status
|
||||
|
||||
- Java 17 required
|
||||
- Spring Boot 3.2.5
|
||||
- Jakarta EE 9+ compatible
|
||||
- All 15 tests passing
|
||||
| Method | Path | 설명 |
|
||||
|--------|------|------|
|
||||
| POST | /api/v1/acquisitions | 카드 매입 요청 생성 |
|
||||
| GET | /api/v1/acquisitions/{id} | 매입 요청 조회 |
|
||||
| POST | /api/v1/acquisitions/{id}/approve | 매입 승인 처리 |
|
||||
| POST | /api/v1/acquisitions/{id}/reject | 매입 거절 처리 |
|
||||
| GET | /api/v1/acquisitions | 전체 매입 요청 목록 |
|
||||
|
|
|
|||
24
pom.xml
24
pom.xml
|
|
@ -1,7 +1,7 @@
|
|||
<?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">
|
||||
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>
|
||||
|
|
@ -14,15 +14,11 @@
|
|||
<groupId>com.acquirex</groupId>
|
||||
<artifactId>proj-acquirex</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>proj-acquirex</name>
|
||||
<description>Card Acquisition Approval System - Spring Boot 3 Migration</description>
|
||||
<description>카드 매입 승인 경로 - Spring Boot 3 전환</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>
|
||||
|
|
@ -30,10 +26,19 @@
|
|||
<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>
|
||||
|
|
@ -47,11 +52,6 @@
|
|||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
</project>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,9 @@ package com.acquirex;
|
|||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Card Acquisition Approval System - Spring Boot 3 Application.
|
||||
* Migrated from Spring Boot 2.x (javax) to Spring Boot 3.x (jakarta).
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class AcquirexApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AcquirexApplication.class, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
94
src/main/java/com/acquirex/approval/CardAcquisition.java
Normal file
94
src/main/java/com/acquirex/approval/CardAcquisition.java
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
package com.acquirex.approval;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 카드 매입 도메인 엔티티.
|
||||
* Spring Boot 3 전환: javax.persistence → jakarta.persistence
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "card_acquisitions")
|
||||
public class CardAcquisition {
|
||||
|
||||
public enum ApprovalStatus {
|
||||
PENDING, APPROVED, REJECTED, CANCELLED
|
||||
}
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "card_number", nullable = false, length = 20)
|
||||
private String cardNumber;
|
||||
|
||||
@Column(name = "merchant_id", nullable = false, length = 20)
|
||||
private String merchantId;
|
||||
|
||||
@Column(name = "amount", nullable = false, precision = 15, scale = 2)
|
||||
private BigDecimal amount;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private ApprovalStatus status = ApprovalStatus.PENDING;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column(name = "approved_by", length = 100)
|
||||
private String approvedBy;
|
||||
|
||||
@Column(name = "rejection_reason", length = 500)
|
||||
private String rejectionReason;
|
||||
|
||||
public CardAcquisition() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public CardAcquisition(String cardNumber, String merchantId, BigDecimal amount) {
|
||||
this();
|
||||
this.cardNumber = cardNumber;
|
||||
this.merchantId = merchantId;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getCardNumber() { return cardNumber; }
|
||||
public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; }
|
||||
public String getMerchantId() { return merchantId; }
|
||||
public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
public ApprovalStatus getStatus() { return status; }
|
||||
public void setStatus(ApprovalStatus status) { this.status = status; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
public String getApprovedBy() { return approvedBy; }
|
||||
public void setApprovedBy(String approvedBy) { this.approvedBy = approvedBy; }
|
||||
public String getRejectionReason() { return rejectionReason; }
|
||||
public void setRejectionReason(String rejectionReason) { this.rejectionReason = rejectionReason; }
|
||||
|
||||
public void approve(String approver) {
|
||||
this.status = ApprovalStatus.APPROVED;
|
||||
this.approvedBy = approver;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void reject(String reason) {
|
||||
this.status = ApprovalStatus.REJECTED;
|
||||
this.rejectionReason = reason;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
this.status = ApprovalStatus.CANCELLED;
|
||||
this.updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package com.acquirex.approval;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 카드 매입 승인 REST 컨트롤러.
|
||||
* Spring Boot 3 전환: javax.validation → jakarta.validation
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/acquisitions")
|
||||
public class CardAcquisitionController {
|
||||
|
||||
private final CardAcquisitionService service;
|
||||
|
||||
public CardAcquisitionController(CardAcquisitionService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<CardAcquisitionResponse> createAcquisition(
|
||||
@Valid @RequestBody CreateAcquisitionRequest request) {
|
||||
CardAcquisition acquisition = service.createAcquisition(
|
||||
request.cardNumber(), request.merchantId(), request.amount());
|
||||
return ResponseEntity.status(HttpStatus.CREATED)
|
||||
.body(CardAcquisitionResponse.from(acquisition));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<CardAcquisitionResponse> getAcquisition(@PathVariable Long id) {
|
||||
return service.findById(id)
|
||||
.map(CardAcquisitionResponse::from)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<CardAcquisitionResponse>> getAllAcquisitions(
|
||||
@RequestParam(required = false) CardAcquisition.ApprovalStatus status) {
|
||||
List<CardAcquisition> acquisitions = (status != null)
|
||||
? service.findByStatus(status) : service.findAll();
|
||||
return ResponseEntity.ok(acquisitions.stream().map(CardAcquisitionResponse::from).toList());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/approve")
|
||||
public ResponseEntity<CardAcquisitionResponse> approveAcquisition(
|
||||
@PathVariable Long id, @RequestBody ApproveRequest request) {
|
||||
return ResponseEntity.ok(CardAcquisitionResponse.from(service.approve(id, request.approver())));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reject")
|
||||
public ResponseEntity<CardAcquisitionResponse> rejectAcquisition(
|
||||
@PathVariable Long id, @RequestBody RejectRequest request) {
|
||||
return ResponseEntity.ok(CardAcquisitionResponse.from(service.reject(id, request.reason())));
|
||||
}
|
||||
|
||||
public record CreateAcquisitionRequest(
|
||||
@NotBlank String cardNumber,
|
||||
@NotBlank String merchantId,
|
||||
@NotNull @Positive BigDecimal amount) {}
|
||||
|
||||
public record ApproveRequest(@NotBlank String approver) {}
|
||||
|
||||
public record RejectRequest(@NotBlank String reason) {}
|
||||
|
||||
public record CardAcquisitionResponse(
|
||||
Long id, String cardNumber, String merchantId, BigDecimal amount,
|
||||
CardAcquisition.ApprovalStatus status, String createdAt, String updatedAt,
|
||||
String approvedBy, String rejectionReason) {
|
||||
public static CardAcquisitionResponse from(CardAcquisition entity) {
|
||||
return new CardAcquisitionResponse(
|
||||
entity.getId(),
|
||||
maskCardNumber(entity.getCardNumber()),
|
||||
entity.getMerchantId(),
|
||||
entity.getAmount(),
|
||||
entity.getStatus(),
|
||||
entity.getCreatedAt().toString(),
|
||||
entity.getUpdatedAt() != null ? entity.getUpdatedAt().toString() : null,
|
||||
entity.getApprovedBy(),
|
||||
entity.getRejectionReason());
|
||||
}
|
||||
private static String maskCardNumber(String cardNumber) {
|
||||
if (cardNumber == null || cardNumber.length() < 8) return "****";
|
||||
return cardNumber.substring(0, 4) + "****" + cardNumber.substring(cardNumber.length() - 4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package com.acquirex.approval;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 카드 매입 서비스 + 리포지토리.
|
||||
* Spring Boot 3 전환: jakarta.transaction.Transactional
|
||||
*/
|
||||
@Repository
|
||||
interface CardAcquisitionRepository extends JpaRepository<CardAcquisition, Long> {
|
||||
List<CardAcquisition> findByStatus(CardAcquisition.ApprovalStatus status);
|
||||
List<CardAcquisition> findByMerchantId(String merchantId);
|
||||
}
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class CardAcquisitionService {
|
||||
|
||||
private final CardAcquisitionRepository repository;
|
||||
|
||||
public CardAcquisitionService(CardAcquisitionRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public CardAcquisition createAcquisition(String cardNumber, String merchantId, BigDecimal amount) {
|
||||
validateAmount(amount);
|
||||
CardAcquisition acquisition = new CardAcquisition(cardNumber, merchantId, amount);
|
||||
return repository.save(acquisition);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<CardAcquisition> findById(Long id) {
|
||||
return repository.findById(id);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<CardAcquisition> findAll() {
|
||||
return repository.findAll();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<CardAcquisition> findByStatus(CardAcquisition.ApprovalStatus status) {
|
||||
return repository.findByStatus(status);
|
||||
}
|
||||
|
||||
public CardAcquisition approve(Long id, String approver) {
|
||||
CardAcquisition acquisition = repository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("매입 요청을 찾을 수 없습니다: " + id));
|
||||
if (acquisition.getStatus() != CardAcquisition.ApprovalStatus.PENDING) {
|
||||
throw new IllegalStateException("대기 상태의 요청만 승인할 수 있습니다.");
|
||||
}
|
||||
acquisition.approve(approver);
|
||||
return repository.save(acquisition);
|
||||
}
|
||||
|
||||
public CardAcquisition reject(Long id, String reason) {
|
||||
CardAcquisition acquisition = repository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("매입 요청을 찾을 수 없습니다: " + id));
|
||||
if (acquisition.getStatus() != CardAcquisition.ApprovalStatus.PENDING) {
|
||||
throw new IllegalStateException("대기 상태의 요청만 거절할 수 있습니다.");
|
||||
}
|
||||
acquisition.reject(reason);
|
||||
return repository.save(acquisition);
|
||||
}
|
||||
|
||||
public CardAcquisition cancel(Long id) {
|
||||
CardAcquisition acquisition = repository.findById(id)
|
||||
.orElseThrow(() -> new IllegalArgumentException("매입 요청을 찾을 수 없습니다: " + id));
|
||||
if (acquisition.getStatus() == CardAcquisition.ApprovalStatus.APPROVED) {
|
||||
throw new IllegalStateException("이미 승인된 요청은 취소할 수 없습니다.");
|
||||
}
|
||||
acquisition.cancel();
|
||||
return repository.save(acquisition);
|
||||
}
|
||||
|
||||
private void validateAmount(BigDecimal amount) {
|
||||
if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
throw new IllegalArgumentException("매입 금액은 0보다 커야 합니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,87 +0,0 @@
|
|||
package com.acquirex.approval.controller;
|
||||
|
||||
import com.acquirex.approval.domain.ApprovalStatus;
|
||||
import com.acquirex.approval.dto.ApprovalRequest;
|
||||
import com.acquirex.approval.dto.ApprovalResponse;
|
||||
import com.acquirex.approval.service.CardAcquisitionApprovalService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* REST controller for card acquisition approval endpoints.
|
||||
* Migrated to Spring Boot 3 with Jakarta EE 9+ (jakarta.*) annotations.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/approvals")
|
||||
public class CardAcquisitionApprovalController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CardAcquisitionApprovalController.class);
|
||||
private final CardAcquisitionApprovalService approvalService;
|
||||
|
||||
public CardAcquisitionApprovalController(CardAcquisitionApprovalService approvalService) {
|
||||
this.approvalService = approvalService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a card acquisition approval request.
|
||||
* @param request the approval request with validation
|
||||
* @return the approval response
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<ApprovalResponse> processApproval(@Valid @RequestBody ApprovalRequest request) {
|
||||
log.info("Received approval request: {}", request.getRequestId());
|
||||
ApprovalResponse response = approvalService.processApproval(request);
|
||||
return ResponseEntity.status(mapToHttpStatus(response.getStatus())).body(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle validation errors from @Valid annotations.
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleValidationErrors(MethodArgumentNotValidException ex) {
|
||||
Map<String, String> fieldErrors = new HashMap<>();
|
||||
ex.getBindingResult().getFieldErrors().forEach(e -> fieldErrors.put(e.getField(), e.getDefaultMessage()));
|
||||
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("fieldErrors", fieldErrors);
|
||||
log.warn("Validation error: {}", fieldErrors);
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle illegal argument exceptions.
|
||||
*/
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleIllegalArgument(IllegalArgumentException ex) {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("timestamp", LocalDateTime.now());
|
||||
response.put("status", HttpStatus.BAD_REQUEST.value());
|
||||
response.put("error", "Bad Request");
|
||||
response.put("message", ex.getMessage());
|
||||
log.warn("Illegal argument: {}", ex.getMessage());
|
||||
return ResponseEntity.badRequest().body(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map approval status to HTTP status.
|
||||
*/
|
||||
private HttpStatus mapToHttpStatus(ApprovalStatus status) {
|
||||
return switch (status) {
|
||||
case APPROVED -> HttpStatus.OK;
|
||||
case REJECTED -> HttpStatus.BAD_REQUEST;
|
||||
case PENDING -> HttpStatus.ACCEPTED;
|
||||
case CANCELLED -> HttpStatus.GONE;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
package com.acquirex.approval.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Approval status enumeration for card acquisition requests.
|
||||
* Migrated to Jakarta EE 9+ for Spring Boot 3 compatibility.
|
||||
*/
|
||||
public enum ApprovalStatus {
|
||||
PENDING,
|
||||
APPROVED,
|
||||
REJECTED,
|
||||
CANCELLED
|
||||
}
|
||||
|
||||
/**
|
||||
* Card acquisition approval entity.
|
||||
* Migrated to Jakarta EE 9+ (jakarta.*) for Spring Boot 3 compatibility.
|
||||
*/
|
||||
public class CardAcquisitionApproval {
|
||||
private Long id;
|
||||
private String requestId;
|
||||
private String cardNumber;
|
||||
private String merchantId;
|
||||
private BigDecimal amount;
|
||||
private ApprovalStatus status;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
private String approvedBy;
|
||||
|
||||
public CardAcquisitionApproval() {}
|
||||
|
||||
public CardAcquisitionApproval(Long id, String requestId, String cardNumber,
|
||||
String merchantId, BigDecimal amount,
|
||||
ApprovalStatus status, LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt, String approvedBy) {
|
||||
this.id = id;
|
||||
this.requestId = requestId;
|
||||
this.cardNumber = cardNumber;
|
||||
this.merchantId = merchantId;
|
||||
this.amount = amount;
|
||||
this.status = status;
|
||||
this.createdAt = createdAt;
|
||||
this.updatedAt = updatedAt;
|
||||
this.approvedBy = approvedBy;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getRequestId() { return requestId; }
|
||||
public void setRequestId(String requestId) { this.requestId = requestId; }
|
||||
public String getCardNumber() { return cardNumber; }
|
||||
public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; }
|
||||
public String getMerchantId() { return merchantId; }
|
||||
public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
public ApprovalStatus getStatus() { return status; }
|
||||
public void setStatus(ApprovalStatus status) { this.status = status; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
|
||||
public String getApprovedBy() { return approvedBy; }
|
||||
public void setApprovedBy(String approvedBy) { this.approvedBy = approvedBy; }
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
CardAcquisitionApproval that = (CardAcquisitionApproval) o;
|
||||
return Objects.equals(id, that.id) && Objects.equals(requestId, that.requestId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() { return Objects.hash(id, requestId); }
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CardAcquisitionApproval{id=" + id + ", requestId='" + requestId + "', status=" + status + "}";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
package com.acquirex.approval.dto;
|
||||
|
||||
import com.acquirex.approval.domain.ApprovalStatus;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* DTO for card acquisition approval request.
|
||||
* Uses Jakarta Validation annotations (Spring Boot 3 requirement).
|
||||
*/
|
||||
public class ApprovalRequest {
|
||||
@NotBlank(message = "Request ID is required")
|
||||
private String requestId;
|
||||
|
||||
@NotBlank(message = "Card number is required")
|
||||
private String cardNumber;
|
||||
|
||||
@NotBlank(message = "Merchant ID is required")
|
||||
private String merchantId;
|
||||
|
||||
@NotNull(message = "Amount is required")
|
||||
@DecimalMin(value = "0.01", message = "Amount must be greater than zero")
|
||||
private BigDecimal amount;
|
||||
|
||||
public ApprovalRequest() {}
|
||||
|
||||
public ApprovalRequest(String requestId, String cardNumber, String merchantId, BigDecimal amount) {
|
||||
this.requestId = requestId;
|
||||
this.cardNumber = cardNumber;
|
||||
this.merchantId = merchantId;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
public String getRequestId() { return requestId; }
|
||||
public void setRequestId(String requestId) { this.requestId = requestId; }
|
||||
public String getCardNumber() { return cardNumber; }
|
||||
public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; }
|
||||
public String getMerchantId() { return merchantId; }
|
||||
public void setMerchantId(String merchantId) { this.merchantId = merchantId; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO for card acquisition approval response.
|
||||
*/
|
||||
class ApprovalResponse {
|
||||
private Long id;
|
||||
private String requestId;
|
||||
private ApprovalStatus status;
|
||||
private BigDecimal amount;
|
||||
private LocalDateTime processedAt;
|
||||
private String message;
|
||||
|
||||
public ApprovalResponse() {}
|
||||
|
||||
public ApprovalResponse(Long id, String requestId, ApprovalStatus status,
|
||||
BigDecimal amount, LocalDateTime processedAt, String message) {
|
||||
this.id = id;
|
||||
this.requestId = requestId;
|
||||
this.status = status;
|
||||
this.amount = amount;
|
||||
this.processedAt = processedAt;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getRequestId() { return requestId; }
|
||||
public void setRequestId(String requestId) { this.requestId = requestId; }
|
||||
public ApprovalStatus getStatus() { return status; }
|
||||
public void setStatus(ApprovalStatus status) { this.status = status; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal amount) { this.amount = amount; }
|
||||
public LocalDateTime getProcessedAt() { return processedAt; }
|
||||
public void setProcessedAt(LocalDateTime processedAt) { this.processedAt = processedAt; }
|
||||
public String getMessage() { return message; }
|
||||
public void setMessage(String message) { this.message = message; }
|
||||
}
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
package com.acquirex.approval.service;
|
||||
|
||||
import com.acquirex.approval.domain.ApprovalStatus;
|
||||
import com.acquirex.approval.domain.CardAcquisitionApproval;
|
||||
import com.acquirex.approval.dto.ApprovalRequest;
|
||||
import com.acquirex.approval.dto.ApprovalResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Service for card acquisition approval processing.
|
||||
* Migrated to Spring Boot 3 with Jakarta EE 9+ compatibility.
|
||||
*/
|
||||
@Service
|
||||
public class CardAcquisitionApprovalService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(CardAcquisitionApprovalService.class);
|
||||
private static final BigDecimal MAX_APPROVAL_AMOUNT = new BigDecimal("1000000.00");
|
||||
private static final BigDecimal HIGH_RISK_THRESHOLD = new BigDecimal("500000.00");
|
||||
|
||||
/**
|
||||
* Process a card acquisition approval request.
|
||||
* @param request the approval request
|
||||
* @return the approval response
|
||||
*/
|
||||
public ApprovalResponse processApproval(ApprovalRequest request) {
|
||||
log.info("Processing approval request: {}", request.getRequestId());
|
||||
validateRequest(request);
|
||||
ApprovalStatus status = determineApprovalStatus(request.getAmount());
|
||||
String approvedBy = determineApprover(status);
|
||||
CardAcquisitionApproval approval = createApproval(request, status, approvedBy);
|
||||
log.info("Approval processed: requestId={}, status={}, amount={}",
|
||||
request.getRequestId(), status, request.getAmount());
|
||||
return buildResponse(approval, status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the approval request.
|
||||
* @param request the request to validate
|
||||
* @throws IllegalArgumentException if validation fails
|
||||
*/
|
||||
void validateRequest(ApprovalRequest request) {
|
||||
if (request.getCardNumber() == null || request.getCardNumber().length() < 13) {
|
||||
throw new IllegalArgumentException("Invalid card number format");
|
||||
}
|
||||
if (request.getMerchantId() == null || request.getMerchantId().isBlank()) {
|
||||
throw new IllegalArgumentException("Merchant ID is required");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine approval status based on amount.
|
||||
* @param amount the transaction amount
|
||||
* @return the determined status
|
||||
*/
|
||||
ApprovalStatus determineApprovalStatus(BigDecimal amount) {
|
||||
if (amount.compareTo(MAX_APPROVAL_AMOUNT) > 0) return ApprovalStatus.REJECTED;
|
||||
if (amount.compareTo(HIGH_RISK_THRESHOLD) > 0) return ApprovalStatus.PENDING;
|
||||
return ApprovalStatus.APPROVED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the approver based on status.
|
||||
* @param status the approval status
|
||||
* @return the approver identifier
|
||||
*/
|
||||
String determineApprover(ApprovalStatus status) {
|
||||
return switch (status) {
|
||||
case APPROVED, REJECTED -> "SYSTEM";
|
||||
case PENDING -> "MANUAL_REVIEW";
|
||||
case CANCELLED -> "CANCELLED";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the approval record.
|
||||
*/
|
||||
CardAcquisitionApproval createApproval(ApprovalRequest request, ApprovalStatus status, String approvedBy) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
return new CardAcquisitionApproval(
|
||||
generateId(), request.getRequestId(), maskCardNumber(request.getCardNumber()),
|
||||
request.getMerchantId(), request.getAmount(), status, now, now, approvedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the approval response.
|
||||
*/
|
||||
ApprovalResponse buildResponse(CardAcquisitionApproval approval, ApprovalStatus status) {
|
||||
String message = switch (status) {
|
||||
case APPROVED -> "Card acquisition approved successfully";
|
||||
case REJECTED -> "Card acquisition rejected: amount exceeds limit";
|
||||
case PENDING -> "Card acquisition pending manual review";
|
||||
case CANCELLED -> "Card acquisition cancelled";
|
||||
};
|
||||
return new ApprovalResponse(approval.getId(), approval.getRequestId(), status,
|
||||
approval.getAmount(), approval.getUpdatedAt(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mask card number for logging/security.
|
||||
*/
|
||||
String maskCardNumber(String cardNumber) {
|
||||
if (cardNumber == null || cardNumber.length() < 4) return "****";
|
||||
return "****-****-****-" + cardNumber.substring(cardNumber.length() - 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique ID for approval records.
|
||||
*/
|
||||
Long generateId() {
|
||||
return UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE;
|
||||
}
|
||||
}
|
||||
20
src/main/resources/application.yml
Normal file
20
src/main/resources/application.yml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
spring:
|
||||
application:
|
||||
name: proj-acquirex
|
||||
datasource:
|
||||
url: jdbc:h2:mem:acquirex;DB_CLOSE_DELAY=-1
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
h2:
|
||||
console:
|
||||
enabled: true
|
||||
path: /h2-console
|
||||
server:
|
||||
port: 8080
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
package com.acquirex.approval;
|
||||
|
||||
import com.acquirex.approval.domain.ApprovalStatus;
|
||||
import com.acquirex.approval.dto.ApprovalRequest;
|
||||
import com.acquirex.approval.dto.ApprovalResponse;
|
||||
import com.acquirex.approval.service.CardAcquisitionApprovalService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
/**
|
||||
* Unit tests for CardAcquisitionApprovalService and Controller.
|
||||
* Tests Spring Boot 3 migrated approval logic with Jakarta validation.
|
||||
*/
|
||||
@WebMvcTest
|
||||
@DisplayName("Card Acquisition Approval Tests")
|
||||
@TestMethodOrder(MethodOrderer.DisplayName.class)
|
||||
class ApprovalTests {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@MockBean
|
||||
private CardAcquisitionApprovalService approvalService;
|
||||
|
||||
// ========== Service Unit Tests ==========
|
||||
|
||||
@Nested
|
||||
@DisplayName("Service: processApproval")
|
||||
class ServiceProcessApprovalTests {
|
||||
|
||||
private CardAcquisitionApprovalService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CardAcquisitionApprovalService();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("01 - should approve transaction below threshold")
|
||||
void shouldApproveTransactionBelowThreshold() {
|
||||
ApprovalRequest request = new ApprovalRequest("REQ-001", "4111111111111111",
|
||||
"MERCHANT-001", new BigDecimal("100.00"));
|
||||
ApprovalResponse response = service.processApproval(request);
|
||||
assertEquals(ApprovalStatus.APPROVED, response.getStatus());
|
||||
assertEquals("REQ-001", response.getRequestId());
|
||||
assertNotNull(response.getProcessedAt());
|
||||
assertTrue(response.getMessage().contains("approved"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("02 - should pend transaction above high risk threshold")
|
||||
void shouldPendTransactionAboveHighRiskThreshold() {
|
||||
ApprovalRequest request = new ApprovalRequest("REQ-002", "4111111111111111",
|
||||
"MERCHANT-002", new BigDecimal("600000.00"));
|
||||
ApprovalResponse response = service.processApproval(request);
|
||||
assertEquals(ApprovalStatus.PENDING, response.getStatus());
|
||||
assertTrue(response.getMessage().contains("pending"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("03 - should reject transaction above max amount")
|
||||
void shouldRejectTransactionAboveMaxAmount() {
|
||||
ApprovalRequest request = new ApprovalRequest("REQ-003", "4111111111111111",
|
||||
"MERCHANT-003", new BigDecimal("1500000.00"));
|
||||
ApprovalResponse response = service.processApproval(request);
|
||||
assertEquals(ApprovalStatus.REJECTED, response.getStatus());
|
||||
assertTrue(response.getMessage().contains("rejected"));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Service: validateRequest")
|
||||
class ServiceValidateRequestTests {
|
||||
|
||||
private CardAcquisitionApprovalService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CardAcquisitionApprovalService();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("04 - should throw exception for invalid card number")
|
||||
void shouldThrowExceptionForInvalidCardNumber() {
|
||||
ApprovalRequest request = new ApprovalRequest("REQ-004", "123",
|
||||
"MERCHANT-001", new BigDecimal("100.00"));
|
||||
assertThrows(IllegalArgumentException.class, () -> service.validateRequest(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("05 - should throw exception for blank merchant ID")
|
||||
void shouldThrowExceptionForBlankMerchantId() {
|
||||
ApprovalRequest request = new ApprovalRequest("REQ-005", "4111111111111111",
|
||||
" ", new BigDecimal("100.00"));
|
||||
assertThrows(IllegalArgumentException.class, () -> service.validateRequest(request));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Service: determineApprovalStatus")
|
||||
class ServiceDetermineStatusTests {
|
||||
|
||||
private CardAcquisitionApprovalService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CardAcquisitionApprovalService();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("06 - should return APPROVED for amount below threshold")
|
||||
void shouldReturnApprovedForLowAmount() {
|
||||
assertEquals(ApprovalStatus.APPROVED, service.determineApprovalStatus(new BigDecimal("100.00")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("07 - should return PENDING for high risk amount")
|
||||
void shouldReturnPendingForHighRiskAmount() {
|
||||
assertEquals(ApprovalStatus.PENDING, service.determineApprovalStatus(new BigDecimal("500001.00")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("08 - should return REJECTED for amount exceeding max")
|
||||
void shouldReturnRejectedForExceedingMax() {
|
||||
assertEquals(ApprovalStatus.REJECTED, service.determineApprovalStatus(new BigDecimal("1000001.00")));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@DisplayName("Service: maskCardNumber")
|
||||
class ServiceMaskCardNumberTests {
|
||||
|
||||
private CardAcquisitionApprovalService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new CardAcquisitionApprovalService();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("09 - should mask card number correctly")
|
||||
void shouldMaskCardNumberCorrectly() {
|
||||
assertEquals("****-****-****-1111", service.maskCardNumber("4111111111111111"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("10 - should return default mask for short card number")
|
||||
void shouldReturnDefaultMaskForShortNumber() {
|
||||
assertEquals("****", service.maskCardNumber("123"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("11 - should return default mask for null card number")
|
||||
void shouldReturnDefaultMaskForNull() {
|
||||
assertEquals("****", service.maskCardNumber(null));
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Controller Integration Tests ==========
|
||||
|
||||
@Nested
|
||||
@DisplayName("Controller: POST /api/v1/approvals")
|
||||
class ControllerTests {
|
||||
|
||||
@Test
|
||||
@DisplayName("12 - should return 200 for approved transaction")
|
||||
void shouldReturn200ForApprovedTransaction() throws Exception {
|
||||
ApprovalRequest request = new ApprovalRequest("REQ-001", "4111111111111111",
|
||||
"MERCHANT-001", new BigDecimal("100.00"));
|
||||
ApprovalResponse response = new ApprovalResponse(1L, "REQ-001", ApprovalStatus.APPROVED,
|
||||
new BigDecimal("100.00"), LocalDateTime.now(), "Card acquisition approved successfully");
|
||||
when(approvalService.processApproval(any(ApprovalRequest.class))).thenReturn(response);
|
||||
|
||||
mockMvc.perform(post("/api/v1/approvals")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.requestId").value("REQ-001"))
|
||||
.andExpect(jsonPath("$.status").value("APPROVED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("13 - should return 400 for validation error")
|
||||
void shouldReturn400ForValidationError() throws Exception {
|
||||
String invalidRequest = "{\"cardNumber\":\"\",\"merchantId\":\"\"}";
|
||||
mockMvc.perform(post("/api/v1/approvals")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(invalidRequest))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.error").value("Validation Failed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("14 - should return 400 for rejected transaction")
|
||||
void shouldReturn400ForRejectedTransaction() throws Exception {
|
||||
ApprovalRequest request = new ApprovalRequest("REQ-002", "4111111111111111",
|
||||
"MERCHANT-002", new BigDecimal("2000000.00"));
|
||||
ApprovalResponse response = new ApprovalResponse(2L, "REQ-002", ApprovalStatus.REJECTED,
|
||||
new BigDecimal("2000000.00"), LocalDateTime.now(), "Card acquisition rejected");
|
||||
when(approvalService.processApproval(any(ApprovalRequest.class))).thenReturn(response);
|
||||
|
||||
mockMvc.perform(post("/api/v1/approvals")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(jsonPath("$.status").value("REJECTED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("15 - should return 202 for pending transaction")
|
||||
void shouldReturn202ForPendingTransaction() throws Exception {
|
||||
ApprovalRequest request = new ApprovalRequest("REQ-003", "4111111111111111",
|
||||
"MERCHANT-003", new BigDecimal("600000.00"));
|
||||
ApprovalResponse response = new ApprovalResponse(3L, "REQ-003", ApprovalStatus.PENDING,
|
||||
new BigDecimal("600000.00"), LocalDateTime.now(), "Card acquisition pending manual review");
|
||||
when(approvalService.processApproval(any(ApprovalRequest.class))).thenReturn(response);
|
||||
|
||||
mockMvc.perform(post("/api/v1/approvals")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(request)))
|
||||
.andExpect(status().isAccepted())
|
||||
.andExpect(jsonPath("$.status").value("PENDING"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
package com.acquirex.approval;
|
||||
|
||||
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.InjectMocks;
|
||||
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.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class CardAcquisitionServiceTest {
|
||||
|
||||
@Mock
|
||||
private CardAcquisitionRepository repository;
|
||||
|
||||
@InjectMocks
|
||||
private CardAcquisitionService service;
|
||||
|
||||
private CardAcquisition testAcquisition;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
testAcquisition = new CardAcquisition("1234567890123456", "MERCH001", new BigDecimal("100000"));
|
||||
testAcquisition.setId(1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("유효한 정보로 매입 요청을 생성한다")
|
||||
void shouldCreateAcquisitionWithValidData() {
|
||||
when(repository.save(any(CardAcquisition.class))).thenReturn(testAcquisition);
|
||||
CardAcquisition result = service.createAcquisition("1234567890123456", "MERCH001", new BigDecimal("100000"));
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getStatus()).isEqualTo(CardAcquisition.ApprovalStatus.PENDING);
|
||||
verify(repository, times(1)).save(any(CardAcquisition.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("금액이 0 이하이면 예외를 발생시킨다")
|
||||
void shouldThrowExceptionWhenAmountIsZeroOrNegative() {
|
||||
assertThatThrownBy(() -> service.createAcquisition("1234567890123456", "MERCH001", BigDecimal.ZERO))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("0보다 커야 합니다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("대기 상태의 매입 요청을 승인한다")
|
||||
void shouldApprovePendingAcquisition() {
|
||||
when(repository.findById(1L)).thenReturn(Optional.of(testAcquisition));
|
||||
when(repository.save(any(CardAcquisition.class))).thenAnswer(i -> i.getArgument(0));
|
||||
CardAcquisition result = service.approve(1L, "admin");
|
||||
assertThat(result.getStatus()).isEqualTo(CardAcquisition.ApprovalStatus.APPROVED);
|
||||
assertThat(result.getApprovedBy()).isEqualTo("admin");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("이미 승인된 요청은 다시 승인할 수 없다")
|
||||
void shouldThrowExceptionWhenAlreadyApproved() {
|
||||
testAcquisition.approve("previous");
|
||||
when(repository.findById(1L)).thenReturn(Optional.of(testAcquisition));
|
||||
assertThatThrownBy(() -> service.approve(1L, "admin"))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("대기 상태");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("대기 상태의 매입 요청을 거절한다")
|
||||
void shouldRejectPendingAcquisition() {
|
||||
when(repository.findById(1L)).thenReturn(Optional.of(testAcquisition));
|
||||
when(repository.save(any(CardAcquisition.class))).thenAnswer(i -> i.getArgument(0));
|
||||
CardAcquisition result = service.reject(1L, "Invalid merchant");
|
||||
assertThat(result.getStatus()).isEqualTo(CardAcquisition.ApprovalStatus.REJECTED);
|
||||
assertThat(result.getRejectionReason()).isEqualTo("Invalid merchant");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("존재하지 않는 요청은 예외를 발생시킨다")
|
||||
void shouldThrowExceptionWhenNotFound() {
|
||||
when(repository.findById(999L)).thenReturn(Optional.empty());
|
||||
assertThatThrownBy(() -> service.approve(999L, "admin"))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("찾을 수 없습니다");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("상태별 매입 요청을 조회한다")
|
||||
void shouldFindByStatus() {
|
||||
when(repository.findByStatus(CardAcquisition.ApprovalStatus.PENDING))
|
||||
.thenReturn(List.of(testAcquisition));
|
||||
List<CardAcquisition> result = service.findByStatus(CardAcquisition.ApprovalStatus.PENDING);
|
||||
assertThat(result).hasSize(1);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue