From 8d6e91e89d50bbef174ae24145e5195c5159586e Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 07:29:31 +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 | 57 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 pom.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..afcab6c --- /dev/null +++ b/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.acquirex + proj-acquirex + 1.0.0-SNAPSHOT + proj-acquirex + Card Acquisition Approval System - Spring Boot 3 + + + 17 + + + + + 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 + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file From c9483167fc82622b187d1383befaea72132dd460 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 07:29:32 +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 | 12 ++++++++++++ 1 file changed, 12 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..dedeed3 --- /dev/null +++ b/src/main/java/com/acquirex/AcquirexApplication.java @@ -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); + } +} \ No newline at end of file From 1696f372a4a7cb11bbf52c59b6170c6f96ff367d Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 07:29:33 +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 --- .../acquirex/domain/AcquisitionApproval.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/main/java/com/acquirex/domain/AcquisitionApproval.java diff --git a/src/main/java/com/acquirex/domain/AcquisitionApproval.java b/src/main/java/com/acquirex/domain/AcquisitionApproval.java new file mode 100644 index 0000000..fe48635 --- /dev/null +++ b/src/main/java/com/acquirex/domain/AcquisitionApproval.java @@ -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; } +} \ No newline at end of file From 7040dace4c86a3c59df8fb922f5677368236e710 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 07:29:34 +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 --- .../AcquisitionApprovalRepository.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/main/java/com/acquirex/repository/AcquisitionApprovalRepository.java diff --git a/src/main/java/com/acquirex/repository/AcquisitionApprovalRepository.java b/src/main/java/com/acquirex/repository/AcquisitionApprovalRepository.java new file mode 100644 index 0000000..603a16d --- /dev/null +++ b/src/main/java/com/acquirex/repository/AcquisitionApprovalRepository.java @@ -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 { + + List findByMerchantId(String merchantId); + + List findByStatus(ApprovalStatus status); + + Optional findByApprovalCode(String approvalCode); + + List findByMerchantIdAndStatus(String merchantId, ApprovalStatus status); + + List findByRequestedAtBetween(LocalDateTime start, LocalDateTime end); +} \ No newline at end of file From 692278427d756c72058648ba7dd73e4d76ac77d4 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 07:29:35 +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 --- .../service/AcquisitionApprovalService.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/main/java/com/acquirex/service/AcquisitionApprovalService.java diff --git a/src/main/java/com/acquirex/service/AcquisitionApprovalService.java b/src/main/java/com/acquirex/service/AcquisitionApprovalService.java new file mode 100644 index 0000000..6d47d2e --- /dev/null +++ b/src/main/java/com/acquirex/service/AcquisitionApprovalService.java @@ -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 getPendingApprovals() { + return repository.findByStatus(ApprovalStatus.PENDING) + .stream() + .map(this::toResponse) + .collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public List 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() + ); + } +} \ No newline at end of file From 9e8112e4d7222bd327aaa7cb4147c66e3b801baf Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 07:29:36 +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 --- .../AcquisitionApprovalController.java | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 src/main/java/com/acquirex/controller/AcquisitionApprovalController.java diff --git a/src/main/java/com/acquirex/controller/AcquisitionApprovalController.java b/src/main/java/com/acquirex/controller/AcquisitionApprovalController.java new file mode 100644 index 0000000..866a558 --- /dev/null +++ b/src/main/java/com/acquirex/controller/AcquisitionApprovalController.java @@ -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 requestApproval(@Valid @RequestBody ApprovalRequest request) { + ApprovalResponse response = service.requestApproval(request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + + @PostMapping("/{id}/approve") + public ResponseEntity approve(@PathVariable Long id) { + ApprovalResponse response = service.processApproval(id, true, null); + return ResponseEntity.ok(response); + } + + @PostMapping("/{id}/reject") + public ResponseEntity reject(@PathVariable Long id, @RequestParam String reason) { + ApprovalResponse response = service.processApproval(id, false, reason); + return ResponseEntity.ok(response); + } + + @GetMapping("/{id}") + public ResponseEntity getApproval(@PathVariable Long id) { + ApprovalResponse response = service.getApproval(id); + return ResponseEntity.ok(response); + } + + @GetMapping("/pending") + public ResponseEntity> getPendingApprovals() { + List responses = service.getPendingApprovals(); + return ResponseEntity.ok(responses); + } + + @GetMapping("/merchant/{merchantId}") + public ResponseEntity> getApprovalsByMerchant(@PathVariable String merchantId) { + List 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> handleApprovalProcessingException(ApprovalProcessingException ex) { + Map 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> handleValidationExceptions(MethodArgumentNotValidException ex) { + Map body = new HashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("status", HttpStatus.BAD_REQUEST.value()); + body.put("error", "Validation Error"); + + Map 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); + } +} \ No newline at end of file From c829679c3d3a79633d7c4bbc29d101b63a7bfc62 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 07:29:38 +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 --- src/main/resources/application.properties | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/main/resources/application.properties diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..8085883 --- /dev/null +++ b/src/main/resources/application.properties @@ -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 \ No newline at end of file From 6cd9fbdf8ce72d8d6448ba1363a93c16731ebe95 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 07:29:39 +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 --- .../AcquisitionApprovalServiceTest.java | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/test/java/com/acquirex/service/AcquisitionApprovalServiceTest.java diff --git a/src/test/java/com/acquirex/service/AcquisitionApprovalServiceTest.java b/src/test/java/com/acquirex/service/AcquisitionApprovalServiceTest.java new file mode 100644 index 0000000..652dc93 --- /dev/null +++ b/src/test/java/com/acquirex/service/AcquisitionApprovalServiceTest.java @@ -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 responses = service.getPendingApprovals(); + assertEquals(2, responses.size()); + } + + @Test + void getApprovalsByMerchant_ReturnsMerchantApprovals() { + when(repository.findByMerchantId("MERCHANT001")).thenReturn(Arrays.asList(savedApproval)); + + List responses = service.getApprovalsByMerchant("MERCHANT001"); + assertEquals(1, responses.size()); + assertEquals("MERCHANT001", responses.get(0).getMerchantId()); + } +} \ No newline at end of file