diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..650208f
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,66 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.5
+
+
+
+ com.acquirex
+ proj-acquirex
+ 1.0.0-SNAPSHOT
+ jar
+ proj-acquirex
+ Card Acquisition Approval System - Spring Boot 3
+
+
+ 17
+ 17
+ 17
+ UTF-8
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ com.h2database
+ h2
+ runtime
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.2.5
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/com/acquirex/AcquirexApplication.java b/src/main/java/com/acquirex/AcquirexApplication.java
new file mode 100644
index 0000000..daf6bca
--- /dev/null
+++ b/src/main/java/com/acquirex/AcquirexApplication.java
@@ -0,0 +1,11 @@
+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);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/acquirex/approval/controller/ApprovalController.java b/src/main/java/com/acquirex/approval/controller/ApprovalController.java
new file mode 100644
index 0000000..ac165c2
--- /dev/null
+++ b/src/main/java/com/acquirex/approval/controller/ApprovalController.java
@@ -0,0 +1,38 @@
+package com.acquirex.approval.controller;
+
+import com.acquirex.approval.dto.ApprovalDto.Request;
+import com.acquirex.approval.dto.ApprovalDto.Response;
+import com.acquirex.approval.service.ApprovalService;
+import jakarta.validation.Valid;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/v1/approvals")
+public class ApprovalController {
+
+ private final ApprovalService approvalService;
+
+ public ApprovalController(ApprovalService approvalService) {
+ this.approvalService = approvalService;
+ }
+
+ @PostMapping
+ public ResponseEntity submitApproval(@Valid @RequestBody Request request) {
+ Response response = approvalService.processApproval(request);
+ return ResponseEntity.status(HttpStatus.CREATED).body(response);
+ }
+
+ @GetMapping("/{id}")
+ public ResponseEntity getApproval(@PathVariable Long id) {
+ var request = approvalService.findById(id);
+ Response response = Response.approved(request.getId(), request.getApprovalCode(), request.getAmount(), request.getCurrency());
+ return ResponseEntity.ok(response);
+ }
+
+ @GetMapping("/stats/pending")
+ public ResponseEntity getPendingCount() {
+ return ResponseEntity.ok(approvalService.countPendingApprovals());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/acquirex/approval/domain/ApprovalRequest.java b/src/main/java/com/acquirex/approval/domain/ApprovalRequest.java
new file mode 100644
index 0000000..926f18f
--- /dev/null
+++ b/src/main/java/com/acquirex/approval/domain/ApprovalRequest.java
@@ -0,0 +1,90 @@
+package com.acquirex.approval.domain;
+
+import jakarta.persistence.*;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Entity
+@Table(name = "approval_requests")
+public class ApprovalRequest {
+
+ public enum ApprovalStatus {
+ PENDING, APPROVED, DECLINED, CANCELLED, ERROR
+ }
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @NotBlank
+ @Column(name = "card_number", length = 19)
+ private String cardNumber;
+
+ @NotBlank
+ @Column(name = "merchant_id", length = 15)
+ private String merchantId;
+
+ @NotNull
+ @Positive
+ @Column(name = "amount", precision = 15, scale = 2)
+ private BigDecimal amount;
+
+ @NotBlank
+ @Column(name = "currency", length = 3)
+ private String currency;
+
+ @Enumerated(EnumType.STRING)
+ @Column(name = "status", length = 20)
+ private ApprovalStatus status;
+
+ @Column(name = "approval_code", length = 10)
+ private String approvalCode;
+
+ @Column(name = "created_at")
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at")
+ private LocalDateTime updatedAt;
+
+ public ApprovalRequest() {}
+
+ public ApprovalRequest(String cardNumber, String merchantId, BigDecimal amount, String currency) {
+ this.cardNumber = cardNumber;
+ this.merchantId = merchantId;
+ this.amount = amount;
+ this.currency = currency;
+ this.status = ApprovalStatus.PENDING;
+ this.createdAt = LocalDateTime.now();
+ }
+
+ @PrePersist
+ protected void onCreate() {
+ createdAt = LocalDateTime.now();
+ updatedAt = LocalDateTime.now();
+ }
+
+ @PreUpdate
+ protected void onUpdate() {
+ updatedAt = LocalDateTime.now();
+ }
+
+ 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 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 LocalDateTime getCreatedAt() { return createdAt; }
+ public LocalDateTime getUpdatedAt() { return updatedAt; }
+}
\ No newline at end of file
diff --git a/src/main/java/com/acquirex/approval/dto/ApprovalDto.java b/src/main/java/com/acquirex/approval/dto/ApprovalDto.java
new file mode 100644
index 0000000..41e34f4
--- /dev/null
+++ b/src/main/java/com/acquirex/approval/dto/ApprovalDto.java
@@ -0,0 +1,102 @@
+package com.acquirex.approval.dto;
+
+import com.acquirex.approval.domain.ApprovalRequest.ApprovalStatus;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Positive;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+public class ApprovalDto {
+
+ public static class Request {
+ @NotBlank(message = "Card number is required")
+ @Pattern(regexp = "^\\d{13,19}$", message = "Invalid card number format")
+ private String cardNumber;
+
+ @NotBlank(message = "Merchant ID is required")
+ @Pattern(regexp = "^[A-Z0-9]{8,15}$", message = "Invalid merchant ID format")
+ private String merchantId;
+
+ @NotNull(message = "Amount is required")
+ @Positive(message = "Amount must be positive")
+ private BigDecimal amount;
+
+ @NotBlank(message = "Currency is required")
+ @Pattern(regexp = "^[A-Z]{3}$", message = "Invalid currency code")
+ private String currency;
+
+ public Request() {}
+
+ public Request(String cardNumber, String merchantId, BigDecimal amount, String currency) {
+ this.cardNumber = cardNumber;
+ this.merchantId = merchantId;
+ this.amount = amount;
+ this.currency = currency;
+ }
+
+ 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 String getCurrency() { return currency; }
+ public void setCurrency(String currency) { this.currency = currency; }
+ }
+
+ public static class Response {
+ private Long requestId;
+ private String approvalCode;
+ private ApprovalStatus status;
+ private BigDecimal amount;
+ private String currency;
+ private String message;
+ private LocalDateTime timestamp;
+
+ public Response() {
+ this.timestamp = LocalDateTime.now();
+ }
+
+ public Response(Long requestId, ApprovalStatus status, String message) {
+ this.requestId = requestId;
+ this.status = status;
+ this.message = message;
+ this.timestamp = LocalDateTime.now();
+ }
+
+ public static Response approved(Long requestId, String approvalCode, BigDecimal amount, String currency) {
+ Response r = new Response();
+ r.requestId = requestId;
+ r.approvalCode = approvalCode;
+ r.status = ApprovalStatus.APPROVED;
+ r.amount = amount;
+ r.currency = currency;
+ r.message = "Approval successful";
+ return r;
+ }
+
+ public static Response declined(Long requestId, String reason) {
+ Response r = new Response();
+ r.requestId = requestId;
+ r.status = ApprovalStatus.DECLINED;
+ r.message = reason;
+ return r;
+ }
+
+ public Long getRequestId() { return requestId; }
+ public void setRequestId(Long requestId) { this.requestId = requestId; }
+ public String getApprovalCode() { return approvalCode; }
+ public void setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; }
+ 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 String getCurrency() { return currency; }
+ public void setCurrency(String currency) { this.currency = currency; }
+ public String getMessage() { return message; }
+ public void setMessage(String message) { this.message = message; }
+ public LocalDateTime getTimestamp() { return timestamp; }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/acquirex/approval/repository/ApprovalRequestRepository.java b/src/main/java/com/acquirex/approval/repository/ApprovalRequestRepository.java
new file mode 100644
index 0000000..ef583f7
--- /dev/null
+++ b/src/main/java/com/acquirex/approval/repository/ApprovalRequestRepository.java
@@ -0,0 +1,20 @@
+package com.acquirex.approval.repository;
+
+import com.acquirex.approval.domain.ApprovalRequest;
+import com.acquirex.approval.domain.ApprovalRequest.ApprovalStatus;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Repository
+public interface ApprovalRequestRepository extends JpaRepository {
+
+ List findByStatus(ApprovalStatus status);
+
+ List findByMerchantId(String merchantId);
+
+ List findByCardNumberAndCreatedAtAfter(String cardNumber, LocalDateTime after);
+
+ long countByStatus(ApprovalStatus status);
+}
\ No newline at end of file
diff --git a/src/main/java/com/acquirex/approval/service/ApprovalService.java b/src/main/java/com/acquirex/approval/service/ApprovalService.java
new file mode 100644
index 0000000..352a706
--- /dev/null
+++ b/src/main/java/com/acquirex/approval/service/ApprovalService.java
@@ -0,0 +1,66 @@
+package com.acquirex.approval.service;
+
+import com.acquirex.approval.domain.ApprovalRequest;
+import com.acquirex.approval.domain.ApprovalRequest.ApprovalStatus;
+import com.acquirex.approval.dto.ApprovalDto.Request;
+import com.acquirex.approval.dto.ApprovalDto.Response;
+import com.acquirex.approval.repository.ApprovalRequestRepository;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.UUID;
+
+@Service
+public class ApprovalService {
+
+ private final ApprovalRequestRepository repository;
+
+ public ApprovalService(ApprovalRequestRepository repository) {
+ this.repository = repository;
+ }
+
+ @Transactional
+ public Response processApproval(Request requestDto) {
+ ApprovalRequest request = new ApprovalRequest(
+ requestDto.getCardNumber(),
+ requestDto.getMerchantId(),
+ requestDto.getAmount(),
+ requestDto.getCurrency()
+ );
+
+ ApprovalRequest saved = repository.save(request);
+
+ if (validateRequest(requestDto)) {
+ String approvalCode = generateApprovalCode();
+ saved.setStatus(ApprovalStatus.APPROVED);
+ saved.setApprovalCode(approvalCode);
+ repository.save(saved);
+ return Response.approved(saved.getId(), approvalCode, saved.getAmount(), saved.getCurrency());
+ } else {
+ saved.setStatus(ApprovalStatus.DECLINED);
+ repository.save(saved);
+ return Response.declined(saved.getId(), "Validation failed");
+ }
+ }
+
+ @Transactional(readOnly = true)
+ public ApprovalRequest findById(Long id) {
+ return repository.findById(id)
+ .orElseThrow(() -> new IllegalArgumentException("Approval request not found: " + id));
+ }
+
+ @Transactional(readOnly = true)
+ public long countPendingApprovals() {
+ return repository.countByStatus(ApprovalStatus.PENDING);
+ }
+
+ private boolean validateRequest(Request dto) {
+ if (dto.getAmount() == null || dto.getAmount().signum() <= 0) return false;
+ if (dto.getCardNumber() == null || dto.getCardNumber().length() < 13) return false;
+ if (dto.getMerchantId() == null || dto.getMerchantId().isEmpty()) return false;
+ return dto.getCurrency() != null && dto.getCurrency().length() == 3;
+ }
+
+ private String generateApprovalCode() {
+ return "APP-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/acquirex/approval/service/ApprovalServiceTest.java b/src/test/java/com/acquirex/approval/service/ApprovalServiceTest.java
new file mode 100644
index 0000000..e88c408
--- /dev/null
+++ b/src/test/java/com/acquirex/approval/service/ApprovalServiceTest.java
@@ -0,0 +1,120 @@
+package com.acquirex.approval.service;
+
+import com.acquirex.approval.domain.ApprovalRequest;
+import com.acquirex.approval.domain.ApprovalRequest.ApprovalStatus;
+import com.acquirex.approval.dto.ApprovalDto.Request;
+import com.acquirex.approval.dto.ApprovalDto.Response;
+import com.acquirex.approval.repository.ApprovalRequestRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.math.BigDecimal;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class ApprovalServiceTest {
+
+ @Mock
+ private ApprovalRequestRepository repository;
+
+ private ApprovalService approvalService;
+
+ @BeforeEach
+ void setUp() {
+ approvalService = new ApprovalService(repository);
+ }
+
+ @Test
+ @DisplayName("Should approve valid request")
+ void processApproval_ValidRequest_ReturnsApproved() {
+ Request dto = new Request("4532123456789012", "MERCHANT001", new BigDecimal("100.00"), "KRW");
+
+ ApprovalRequest savedRequest = new ApprovalRequest(dto.getCardNumber(), dto.getMerchantId(), dto.getAmount(), dto.getCurrency());
+ savedRequest.setId(1L);
+
+ when(repository.save(any(ApprovalRequest.class))).thenReturn(savedRequest);
+
+ Response response = approvalService.processApproval(dto);
+
+ assertNotNull(response);
+ assertEquals(ApprovalStatus.APPROVED, response.getStatus());
+ assertNotNull(response.getApprovalCode());
+ assertTrue(response.getApprovalCode().startsWith("APP-"));
+ verify(repository, times(2)).save(any(ApprovalRequest.class));
+ }
+
+ @Test
+ @DisplayName("Should decline request with invalid amount")
+ void processApproval_InvalidAmount_ReturnsDeclined() {
+ Request dto = new Request("4532123456789012", "MERCHANT001", new BigDecimal("-10.00"), "KRW");
+
+ ApprovalRequest savedRequest = new ApprovalRequest(dto.getCardNumber(), dto.getMerchantId(), dto.getAmount(), dto.getCurrency());
+ savedRequest.setId(1L);
+
+ when(repository.save(any(ApprovalRequest.class))).thenReturn(savedRequest);
+
+ Response response = approvalService.processApproval(dto);
+
+ assertNotNull(response);
+ assertEquals(ApprovalStatus.DECLINED, response.getStatus());
+ assertEquals("Validation failed", response.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should decline request with short card number")
+ void processApproval_ShortCardNumber_ReturnsDeclined() {
+ Request dto = new Request("1234", "MERCHANT001", new BigDecimal("100.00"), "KRW");
+
+ ApprovalRequest savedRequest = new ApprovalRequest(dto.getCardNumber(), dto.getMerchantId(), dto.getAmount(), dto.getCurrency());
+ savedRequest.setId(1L);
+
+ when(repository.save(any(ApprovalRequest.class))).thenReturn(savedRequest);
+
+ Response response = approvalService.processApproval(dto);
+
+ assertNotNull(response);
+ assertEquals(ApprovalStatus.DECLINED, response.getStatus());
+ }
+
+ @Test
+ @DisplayName("Should find approval by ID")
+ void findById_ExistingId_ReturnsRequest() {
+ ApprovalRequest request = new ApprovalRequest("4532123456789012", "MERCHANT001", new BigDecimal("100.00"), "KRW");
+ request.setId(1L);
+ request.setStatus(ApprovalStatus.APPROVED);
+
+ when(repository.findById(1L)).thenReturn(java.util.Optional.of(request));
+
+ ApprovalRequest result = approvalService.findById(1L);
+
+ assertNotNull(result);
+ assertEquals(1L, result.getId());
+ assertEquals(ApprovalStatus.APPROVED, result.getStatus());
+ }
+
+ @Test
+ @DisplayName("Should throw exception for non-existing ID")
+ void findById_NonExistingId_ThrowsException() {
+ when(repository.findById(999L)).thenReturn(java.util.Optional.empty());
+
+ assertThrows(IllegalArgumentException.class, () -> approvalService.findById(999L));
+ }
+
+ @Test
+ @DisplayName("Should count pending approvals")
+ void countPendingApprovals_ReturnsCorrectCount() {
+ when(repository.countByStatus(ApprovalStatus.PENDING)).thenReturn(5L);
+
+ long count = approvalService.countPendingApprovals();
+
+ assertEquals(5L, count);
+ verify(repository).countByStatus(ApprovalStatus.PENDING);
+ }
+}
\ No newline at end of file