From 6c76f600a6ba4f1b2efe7df6ed6fa121717dbb4c Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 10:04:22 +0000 Subject: [PATCH 1/8] =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=A7=A4=EC=9E=85=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EA=B2=BD=EB=A1=9C=20Spring=20Boot=203=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(ACX-E2E-1785394564819)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 pom.xml 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 -- 2.49.1 From ed60901b36c16a846b6324e6bbc5fd1f2006499b Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 10:04:23 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=A7=A4=EC=9E=85=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EA=B2=BD=EB=A1=9C=20Spring=20Boot=203=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(ACX-E2E-1785394564819)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/acquirex/AcquirexApplication.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/com/acquirex/AcquirexApplication.java 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 -- 2.49.1 From 52468c0679dcde52d2eb9aa34c782ae80a832f51 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 10:04:24 +0000 Subject: [PATCH 3/8] =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=A7=A4=EC=9E=85=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EA=B2=BD=EB=A1=9C=20Spring=20Boot=203=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(ACX-E2E-1785394564819)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../approval/domain/ApprovalRequest.java | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/main/java/com/acquirex/approval/domain/ApprovalRequest.java 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 -- 2.49.1 From a116697217d3e49280377224d8d06393e16b425e Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 10:04:26 +0000 Subject: [PATCH 4/8] =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=A7=A4=EC=9E=85=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EA=B2=BD=EB=A1=9C=20Spring=20Boot=203=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(ACX-E2E-1785394564819)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../acquirex/approval/dto/ApprovalDto.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/main/java/com/acquirex/approval/dto/ApprovalDto.java 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 -- 2.49.1 From 3f27fd49b7a7a26483d9638564b60634c9fb1902 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 10:04:27 +0000 Subject: [PATCH 5/8] =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=A7=A4=EC=9E=85=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EA=B2=BD=EB=A1=9C=20Spring=20Boot=203=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(ACX-E2E-1785394564819)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/ApprovalRequestRepository.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/java/com/acquirex/approval/repository/ApprovalRequestRepository.java 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 -- 2.49.1 From 7f2b733f2fadfdd20fb4e47db5e531f5204532d5 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 10:04:29 +0000 Subject: [PATCH 6/8] =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=A7=A4=EC=9E=85=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EA=B2=BD=EB=A1=9C=20Spring=20Boot=203=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(ACX-E2E-1785394564819)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../approval/service/ApprovalService.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/main/java/com/acquirex/approval/service/ApprovalService.java 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 -- 2.49.1 From fda9764f5cda89c7c8e49b6c11b2f77061504c0f Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 10:04:30 +0000 Subject: [PATCH 7/8] =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=A7=A4=EC=9E=85=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EA=B2=BD=EB=A1=9C=20Spring=20Boot=203=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(ACX-E2E-1785394564819)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/ApprovalController.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/java/com/acquirex/approval/controller/ApprovalController.java 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 -- 2.49.1 From 793c7f10ad7ae0ac6defcd18ac9d2b123139f7f2 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 10:04:31 +0000 Subject: [PATCH 8/8] =?UTF-8?q?=EC=B9=B4=EB=93=9C=20=EB=A7=A4=EC=9E=85=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EA=B2=BD=EB=A1=9C=20Spring=20Boot=203=20?= =?UTF-8?q?=EC=A0=84=ED=99=98=20(ACX-E2E-1785394564819)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../approval/service/ApprovalServiceTest.java | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/test/java/com/acquirex/approval/service/ApprovalServiceTest.java 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 -- 2.49.1