diff --git a/proj-acquirex/pom.xml b/proj-acquirex/pom.xml new file mode 100644 index 0000000..0da0de7 --- /dev/null +++ b/proj-acquirex/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.acquirex + proj-acquirex + 1.0.0-SNAPSHOT + jar + proj-acquirex + 카드 매입 승인 경로 - Spring Boot 3 마이그레이션 + + + 17 + 17 + 17 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-test + test + + + jakarta.platform + jakarta.jakartaee-bom + 10.0.0 + pom + import + + + 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/proj-acquirex/src/main/java/com/acquirex/AcquirexApplication.java b/proj-acquirex/src/main/java/com/acquirex/AcquirexApplication.java new file mode 100644 index 0000000..daf6bca --- /dev/null +++ b/proj-acquirex/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/proj-acquirex/src/main/java/com/acquirex/approval/controller/ApprovalController.java b/proj-acquirex/src/main/java/com/acquirex/approval/controller/ApprovalController.java new file mode 100644 index 0000000..dc5bc42 --- /dev/null +++ b/proj-acquirex/src/main/java/com/acquirex/approval/controller/ApprovalController.java @@ -0,0 +1,51 @@ +package com.acquirex.approval.controller; + +import com.acquirex.approval.domain.ApprovalDomain.Request; +import com.acquirex.approval.domain.ApprovalDomain.Response; +import com.acquirex.approval.domain.ApprovalDomain.Status; +import com.acquirex.approval.service.ApprovalService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/v1/approvals") +public class ApprovalController { + private static final Logger log = LoggerFactory.getLogger(ApprovalController.class); + private final ApprovalService approvalService; + + public ApprovalController(ApprovalService approvalService) { + this.approvalService = approvalService; + } + + @PostMapping + public ResponseEntity requestApproval(@Valid @RequestBody Request request) { + log.info("매입 승인 API 호출: {}", request.getTransactionId()); + Response response = approvalService.processApproval(request); + return response.getStatus() == Status.APPROVED + ? ResponseEntity.ok(response) + : ResponseEntity.badRequest().body(response); + } + + @DeleteMapping("/{approvalId}") + public ResponseEntity cancelApproval(@PathVariable String approvalId) { + log.info("매입 취소 API 호출: {}", approvalId); + return ResponseEntity.ok(approvalService.cancelApproval(approvalId)); + } + + @GetMapping("/{approvalId}") + public ResponseEntity getApprovalStatus(@PathVariable String approvalId) { + log.info("승인 상태 조회: {}", approvalId); + return ResponseEntity.ok(approvalService.cancelApproval(approvalId)); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleException(Exception ex, HttpServletRequest request) { + log.error("서버 오류: {}", ex.getMessage(), ex); + return ResponseEntity.internalServerError().body( + java.util.Map.of("error", ex.getMessage(), "path", request.getRequestURI())); + } +} \ No newline at end of file diff --git a/proj-acquirex/src/main/java/com/acquirex/approval/domain/ApprovalDomain.java b/proj-acquirex/src/main/java/com/acquirex/approval/domain/ApprovalDomain.java new file mode 100644 index 0000000..24a976b --- /dev/null +++ b/proj-acquirex/src/main/java/com/acquirex/approval/domain/ApprovalDomain.java @@ -0,0 +1,76 @@ +package com.acquirex.approval.domain; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +/** 카드 매입 승인 도메인 - Jakarta EE 9+ */ +public class ApprovalDomain { + + public enum Status { PENDING, APPROVED, REJECTED, CANCELLED } + + public static class Request { + @NotBlank(message = "거래 ID는 필수입니다") + private String transactionId; + @NotBlank(message = "가맹점 ID는 필수입니다") + private String merchantId; + @NotNull @Positive private BigDecimal amount; + @NotBlank private String cardNumber; + private LocalDateTime requestTime = LocalDateTime.now(); + + public Request() {} + public Request(String transactionId, String merchantId, BigDecimal amount, String cardNumber) { + this.transactionId = transactionId; this.merchantId = merchantId; + this.amount = amount; this.cardNumber = cardNumber; + } + public String getTransactionId() { return transactionId; } + public void setTransactionId(String transactionId) { this.transactionId = transactionId; } + 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 getCardNumber() { return cardNumber; } + public void setCardNumber(String cardNumber) { this.cardNumber = cardNumber; } + public LocalDateTime getRequestTime() { return requestTime; } + } + + public static class Response { + private String approvalId; + private String transactionId; + private Status status; + private String message; + private LocalDateTime approvalTime = LocalDateTime.now(); + + public Response() {} + public Response(String approvalId, String transactionId, Status status, String message) { + this.approvalId = approvalId; this.transactionId = transactionId; + this.status = status; this.message = message; + } + public static Response success(String approvalId, String transactionId) { + return new Response(approvalId, transactionId, Status.APPROVED, "매입 승인 완료"); + } + public static Response rejected(String transactionId, String reason) { + return new Response(null, transactionId, Status.REJECTED, reason); + } + public static Response cancelled(String approvalId) { + return new Response(approvalId, null, Status.CANCELLED, "매입 취소 완료"); + } + public String getApprovalId() { return approvalId; } + public void setApprovalId(String approvalId) { this.approvalId = approvalId; } + public String getTransactionId() { return transactionId; } + public void setTransactionId(String transactionId) { this.transactionId = transactionId; } + public Status getStatus() { return status; } + public void setStatus(Status status) { this.status = status; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } + public LocalDateTime getApprovalTime() { return approvalTime; } + public void setApprovalTime(LocalDateTime approvalTime) { this.approvalTime = approvalTime; } + } + + public static String generateApprovalId() { + return "APR-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + } +} \ No newline at end of file diff --git a/proj-acquirex/src/main/java/com/acquirex/approval/service/ApprovalService.java b/proj-acquirex/src/main/java/com/acquirex/approval/service/ApprovalService.java new file mode 100644 index 0000000..b77af54 --- /dev/null +++ b/proj-acquirex/src/main/java/com/acquirex/approval/service/ApprovalService.java @@ -0,0 +1,43 @@ +package com.acquirex.approval.service; + +import com.acquirex.approval.domain.ApprovalDomain; +import com.acquirex.approval.domain.ApprovalDomain.Request; +import com.acquirex.approval.domain.ApprovalDomain.Response; +import com.acquirex.approval.domain.ApprovalDomain.Status; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +@Service +public class ApprovalService { + private static final Logger log = LoggerFactory.getLogger(ApprovalService.class); + + public Response processApproval(Request request) { + log.info("매입 승인 요청: transactionId={}, merchantId={}, amount={}", + request.getTransactionId(), request.getMerchantId(), request.getAmount()); + + if (!validate(request)) { + return Response.rejected(request.getTransactionId(), "유효성 검증 실패"); + } + + String approvalId = ApprovalDomain.generateApprovalId(); + Response response = Response.success(approvalId, request.getTransactionId()); + log.info("매입 승인 완료: approvalId={}, status={}", approvalId, response.getStatus()); + return response; + } + + public Response cancelApproval(String approvalId) { + log.info("매입 승인 취소: approvalId={}", approvalId); + if (approvalId == null || approvalId.isBlank()) { + return Response.rejected(null, "승인 ID가 유효하지 않습니다"); + } + return Response.cancelled(approvalId); + } + + private boolean validate(Request request) { + if (request == null) return false; + if (request.getAmount() == null || request.getAmount().signum() <= 0) return false; + if (request.getCardNumber() == null || request.getCardNumber().length() < 13) return false; + return true; + } +} \ No newline at end of file diff --git a/proj-acquirex/src/main/resources/application.properties b/proj-acquirex/src/main/resources/application.properties new file mode 100644 index 0000000..a942a35 --- /dev/null +++ b/proj-acquirex/src/main/resources/application.properties @@ -0,0 +1,11 @@ +spring.application.name=proj-acquirex +server.port=8080 +spring.datasource.url=jdbc:h2:mem:acquirex;DB_CLOSE_DELAY=-1 +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console +logging.level.com.acquirex=DEBUG \ No newline at end of file diff --git a/proj-acquirex/src/test/java/com/acquirex/approval/ApprovalServiceTest.java b/proj-acquirex/src/test/java/com/acquirex/approval/ApprovalServiceTest.java new file mode 100644 index 0000000..a1ef3aa --- /dev/null +++ b/proj-acquirex/src/test/java/com/acquirex/approval/ApprovalServiceTest.java @@ -0,0 +1,63 @@ +package com.acquirex.approval; + +import com.acquirex.approval.domain.ApprovalDomain; +import com.acquirex.approval.domain.ApprovalDomain.Request; +import com.acquirex.approval.domain.ApprovalDomain.Response; +import com.acquirex.approval.domain.ApprovalDomain.Status; +import com.acquirex.approval.service.ApprovalService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import java.math.BigDecimal; +import static org.junit.jupiter.api.Assertions.*; + +class ApprovalServiceTest { + private ApprovalService service; + + @BeforeEach + void setUp() { service = new ApprovalService(); } + + @Test + @DisplayName("유효한 요청으로 매입 승인 성공") + void processApproval_ValidRequest_Success() { + Request req = new Request("TXN-001", "MERCHANT-001", new BigDecimal("50000"), "1234567890123456"); + Response res = service.processApproval(req); + assertEquals(Status.APPROVED, res.getStatus()); + assertNotNull(res.getApprovalId()); + assertTrue(res.getApprovalId().startsWith("APR-")); + } + + @Test + @DisplayName("유효하지 않은 금액으로 매입 승인 거절") + void processApproval_InvalidAmount_Rejected() { + Request req = new Request("TXN-002", "MERCHANT-001", new BigDecimal("-1000"), "1234567890123456"); + assertEquals(Status.REJECTED, service.processApproval(req).getStatus()); + } + + @Test + @DisplayName("유효하지 않은 카드번호로 매입 승인 거절") + void processApproval_InvalidCardNumber_Rejected() { + Request req = new Request("TXN-003", "MERCHANT-001", new BigDecimal("50000"), "12345"); + assertEquals(Status.REJECTED, service.processApproval(req).getStatus()); + } + + @Test + @DisplayName("null 요청으로 매입 승인 거절") + void processApproval_NullRequest_Rejected() { + assertEquals(Status.REJECTED, service.processApproval(null).getStatus()); + } + + @Test + @DisplayName("유효한 승인 ID로 취소 성공") + void cancelApproval_ValidApprovalId_Success() { + Response res = service.cancelApproval("APR-12345678"); + assertEquals(Status.CANCELLED, res.getStatus()); + } + + @Test + @DisplayName("유효하지 않은 승인 ID로 취소 거절") + void cancelApproval_InvalidApprovalId_Rejected() { + assertEquals(Status.REJECTED, service.cancelApproval("").getStatus()); + assertEquals(Status.REJECTED, service.cancelApproval(null).getStatus()); + } +} \ No newline at end of file diff --git a/proj-acquirex/src/test/resources/application-test.properties b/proj-acquirex/src/test/resources/application-test.properties new file mode 100644 index 0000000..a7e7b77 --- /dev/null +++ b/proj-acquirex/src/test/resources/application-test.properties @@ -0,0 +1,4 @@ +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1 +spring.datasource.driverClassName=org.h2.Driver +spring.jpa.hibernate.ddl-auto=create-drop +logging.level.com.acquirex=DEBUG \ No newline at end of file