From 01d5ac179f8045feef7f5077dbabd1ea70639ba4 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 09:39:20 +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 | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 pom.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..fa81484 --- /dev/null +++ b/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.acquirex + proj-acquirex + 1.0.0-SNAPSHOT + proj-acquirex + 카드 매입 승인 경로 - Spring Boot 3 Migration + + + 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-data-jpa + + + org.springframework.boot + spring-boot-starter-actuator + + + jakarta.persistence + jakarta.persistence-api + + + jakarta.validation + jakarta.validation-api + + + com.h2database + h2 + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + \ No newline at end of file From 77ecc7ae5d8457953d7fc143a761c2ab1ff7be03 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 09:39:21 +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 --- .../com/acquirex/AcquirexApplication.java | 20 +++++++++++++++++++ 1 file changed, 20 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..489bcaa --- /dev/null +++ b/src/main/java/com/acquirex/AcquirexApplication.java @@ -0,0 +1,20 @@ +package com.acquirex; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 카드 매입 승인 경로 Spring Boot 3 애플리케이션 + * + * Spring Boot 3.x 마이그레이션: + * - Java 17+ 필수 + * - Jakarta EE 9+ (javax.* → jakarta.*) + * - Spring Framework 6.x + */ +@SpringBootApplication +public class AcquirexApplication { + + public static void main(String[] args) { + SpringApplication.run(AcquirexApplication.class, args); + } +} \ No newline at end of file From 38fcb8b62f9db7b71ee7e192ab8c797dda1722c6 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 09:39:22 +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 --- .../java/com/acquirex/domain/Acquisition.java | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 src/main/java/com/acquirex/domain/Acquisition.java diff --git a/src/main/java/com/acquirex/domain/Acquisition.java b/src/main/java/com/acquirex/domain/Acquisition.java new file mode 100644 index 0000000..2e6ffa8 --- /dev/null +++ b/src/main/java/com/acquirex/domain/Acquisition.java @@ -0,0 +1,87 @@ +package com.acquirex.domain; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import lombok.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 카드 매입 정보 도메인 엔티티 + * Jakarta EE 9+ 어노테이션 사용 (Spring Boot 3) + */ +@Entity +@Table(name = "acquisitions") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Acquisition { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotBlank(message = "가맹점 번호는 필수입니다") + @Column(name = "merchant_id", nullable = false, length = 20) + private String merchantId; + + @NotBlank(message = "카드번호는 필수입니다") + @Column(name = "card_number", nullable = false, length = 19) + private String cardNumber; + + @NotNull(message = "매입 금액은 필수입니다") + @Positive(message = "매입 금액은 양수여야 합니다") + @Column(name = "amount", nullable = false, precision = 15, scale = 2) + private BigDecimal amount; + + @NotBlank(message = "거래 번호는 필수입니다") + @Column(name = "transaction_id", nullable = false, unique = true, length = 30) + private String transactionId; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false, length = 20) + private AcquisitionStatus status; + + @Column(name = "approval_code", length = 10) + private String approvalCode; + + @Column(name = "approved_at") + private LocalDateTime approvedAt; + + @Column(name = "created_at", nullable = false, updatable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + createdAt = LocalDateTime.now(); + updatedAt = LocalDateTime.now(); + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } + + public void approve(String approvalCode) { + this.status = AcquisitionStatus.APPROVED; + this.approvalCode = approvalCode; + this.approvedAt = LocalDateTime.now(); + } + + public void reject(String reason) { + this.status = AcquisitionStatus.REJECTED; + } +} + +/** + * 카드 매입 승인 상태 열거형 + */ +enum AcquisitionStatus { + PENDING, APPROVED, REJECTED, CANCELLED +} \ No newline at end of file From fc2e4d049c89ef5823bbc1b7b1f98bd6ee2536aa Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 09:39:24 +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/service/AcquisitionService.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/main/java/com/acquirex/service/AcquisitionService.java diff --git a/src/main/java/com/acquirex/service/AcquisitionService.java b/src/main/java/com/acquirex/service/AcquisitionService.java new file mode 100644 index 0000000..98390bc --- /dev/null +++ b/src/main/java/com/acquirex/service/AcquisitionService.java @@ -0,0 +1,145 @@ +package com.acquirex.service; + +import com.acquirex.domain.Acquisition; +import com.acquirex.domain.AcquisitionStatus; +import com.acquirex.exception.AcquisitionNotFoundException; +import com.acquirex.exception.InvalidAcquisitionStateException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * 카드 매입 승인 서비스 - Spring Boot 3 트랜잭션 관리 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class AcquisitionService { + + private final AcquisitionRepository acquisitionRepository; + + @Transactional + public AcquisitionResponse registerAcquisition(AcquisitionRequest request) { + log.info("매입 요청 등록: merchantId={}, amount={}", request.getMerchantId(), request.getAmount()); + String transactionId = generateTransactionId(); + + Acquisition acquisition = Acquisition.builder() + .merchantId(request.getMerchantId()) + .cardNumber(maskCardNumber(request.getCardNumber())) + .amount(request.getAmount()) + .transactionId(transactionId) + .status(AcquisitionStatus.PENDING) + .build(); + + Acquisition saved = acquisitionRepository.save(acquisition); + log.info("매입 요청 등록 완료: transactionId={}", transactionId); + return toResponse(saved); + } + + @Transactional + public AcquisitionResponse approveAcquisition(String transactionId) { + log.info("매입 승인 처리: transactionId={}", transactionId); + + Acquisition acquisition = acquisitionRepository.findByTransactionId(transactionId) + .orElseThrow(() -> new AcquisitionNotFoundException("매입 정보를 찾을 수 없습니다: " + transactionId)); + + if (acquisition.getStatus() != AcquisitionStatus.PENDING) { + throw new InvalidAcquisitionStateException("대기 상태의 매입만 승인할 수 있습니다. 현재 상태: " + acquisition.getStatus()); + } + + String approvalCode = generateApprovalCode(); + acquisition.approve(approvalCode); + Acquisition saved = acquisitionRepository.save(acquisition); + log.info("매입 승인 완료: transactionId={}, approvalCode={}", transactionId, approvalCode); + return toResponse(saved); + } + + @Transactional + public AcquisitionResponse rejectAcquisition(String transactionId, String reason) { + log.info("매입 거절 처리: transactionId={}, reason={}", transactionId, reason); + + Acquisition acquisition = acquisitionRepository.findByTransactionId(transactionId) + .orElseThrow(() -> new AcquisitionNotFoundException("매입 정보를 찾을 수 없습니다: " + transactionId)); + + if (acquisition.getStatus() != AcquisitionStatus.PENDING) { + throw new InvalidAcquisitionStateException("대기 상태의 매입만 거절할 수 있습니다. 현재 상태: " + acquisition.getStatus()); + } + + acquisition.reject(reason); + return toResponse(acquisitionRepository.save(acquisition)); + } + + @Transactional(readOnly = true) + public AcquisitionResponse getAcquisition(String transactionId) { + return toResponse(acquisitionRepository.findByTransactionId(transactionId) + .orElseThrow(() -> new AcquisitionNotFoundException("매입 정보를 찾을 수 없습니다: " + transactionId))); + } + + @Transactional(readOnly = true) + public List getAcquisitionsByMerchant(String merchantId) { + return acquisitionRepository.findByMerchantId(merchantId).stream().map(this::toResponse).collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public List getPendingAcquisitions() { + return acquisitionRepository.findByStatus(AcquisitionStatus.PENDING).stream().map(this::toResponse).collect(Collectors.toList()); + } + + private String generateTransactionId() { + return "TXN" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 8).toUpperCase(); + } + + private String generateApprovalCode() { + return String.format("%06d", (int) (Math.random() * 1000000)); + } + + private String maskCardNumber(String cardNumber) { + if (cardNumber == null || cardNumber.length() < 10) return cardNumber; + return cardNumber.substring(0, 6) + "****" + cardNumber.substring(cardNumber.length() - 4); + } + + private AcquisitionResponse toResponse(Acquisition a) { + return AcquisitionResponse.builder() + .id(a.getId()).transactionId(a.getTransactionId()).merchantId(a.getMerchantId()) + .cardNumber(a.getCardNumber()).amount(a.getAmount()).status(a.getStatus()) + .approvalCode(a.getApprovalCode()).approvedAt(a.getApprovedAt()) + .createdAt(a.getCreatedAt()).updatedAt(a.getUpdatedAt()).build(); + } +} + +/** DTOs */ +@lombok.Getter @lombok.Setter @lombok.NoArgsConstructor @lombok.AllArgsConstructor @lombok.Builder +class AcquisitionRequest { + @jakarta.validation.constraints.NotBlank @jakarta.validation.constraints.Size(max = 20) + private String merchantId; + @jakarta.validation.constraints.NotBlank @jakarta.validation.constraints.Pattern(regexp = "\\d{13,19}") + private String cardNumber; + @jakarta.validation.constraints.NotNull @jakarta.validation.constraints.DecimalMin("0.01") + private BigDecimal amount; +} + +@lombok.Getter @lombok.Setter @lombok.NoArgsConstructor @lombok.AllArgsConstructor @lombok.Builder +class AcquisitionResponse { + private Long id; private String transactionId; private String merchantId; + private String cardNumber; private BigDecimal amount; private AcquisitionStatus status; + private String approvalCode; private LocalDateTime approvedAt; + private LocalDateTime createdAt; private LocalDateTime updatedAt; +} + +/** Repository */ +@Repository +interface AcquisitionRepository extends JpaRepository { + Optional findByTransactionId(String transactionId); + List findByMerchantId(String merchantId); + List findByStatus(AcquisitionStatus status); +} \ No newline at end of file From acc4062cbfb2ab06db70f8ac24acee65efe1c1aa Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 09:39:25 +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 --- .../controller/AcquisitionController.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/main/java/com/acquirex/controller/AcquisitionController.java diff --git a/src/main/java/com/acquirex/controller/AcquisitionController.java b/src/main/java/com/acquirex/controller/AcquisitionController.java new file mode 100644 index 0000000..5e776bc --- /dev/null +++ b/src/main/java/com/acquirex/controller/AcquisitionController.java @@ -0,0 +1,59 @@ +package com.acquirex.controller; + +import com.acquirex.service.AcquisitionRequest; +import com.acquirex.service.AcquisitionResponse; +import com.acquirex.service.AcquisitionService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * 카드 매입 승인 REST 컨트롤러 - Spring Boot 3 (jakarta.validation) + */ +@RestController +@RequestMapping("/api/v1/acquisitions") +@RequiredArgsConstructor +@Slf4j +public class AcquisitionController { + + private final AcquisitionService acquisitionService; + + @PostMapping + public ResponseEntity registerAcquisition(@Valid @RequestBody AcquisitionRequest request) { + log.info("매입 요청 수신: {}", request); + return ResponseEntity.status(HttpStatus.CREATED).body(acquisitionService.registerAcquisition(request)); + } + + @PostMapping("/{transactionId}/approve") + public ResponseEntity approveAcquisition(@PathVariable String transactionId) { + log.info("매입 승인 요청: transactionId={}", transactionId); + return ResponseEntity.ok(acquisitionService.approveAcquisition(transactionId)); + } + + @PostMapping("/{transactionId}/reject") + public ResponseEntity rejectAcquisition(@PathVariable String transactionId, @RequestBody Map body) { + log.info("매입 거절 요청: transactionId={}", transactionId); + return ResponseEntity.ok(acquisitionService.rejectAcquisition(transactionId, body.getOrDefault("reason", "사유 미지정"))); + } + + @GetMapping("/{transactionId}") + public ResponseEntity getAcquisition(@PathVariable String transactionId) { + log.info("매입 조회 요청: transactionId={}", transactionId); + return ResponseEntity.ok(acquisitionService.getAcquisition(transactionId)); + } + + @GetMapping + public ResponseEntity> getAcquisitions(@RequestParam(required = false) String merchantId) { + log.info("매입 목록 조회: merchantId={}", merchantId); + List responses = (merchantId != null && !merchantId.isBlank()) + ? acquisitionService.getAcquisitionsByMerchant(merchantId) + : acquisitionService.getPendingAcquisitions(); + return ResponseEntity.ok(responses); + } +} \ No newline at end of file From e9cd885700d4707374be4e17f1d9989769c26a3d Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 09:39:27 +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 --- .../exception/GlobalExceptionHandler.java | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/main/java/com/acquirex/exception/GlobalExceptionHandler.java diff --git a/src/main/java/com/acquirex/exception/GlobalExceptionHandler.java b/src/main/java/com/acquirex/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..d25d1b1 --- /dev/null +++ b/src/main/java/com/acquirex/exception/GlobalExceptionHandler.java @@ -0,0 +1,74 @@ +package com.acquirex.exception; + +import lombok.extern.slf4j.Slf4j; +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.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** + * 전역 예외 처리 핸들러 - Spring Boot 3 (jakarta.validation) + */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + @ExceptionHandler(AcquisitionNotFoundException.class) + public ResponseEntity> handleAcquisitionNotFound(AcquisitionNotFoundException ex) { + log.error("매입 정보 없음: {}", ex.getMessage()); + return buildErrorResponse(HttpStatus.NOT_FOUND, ex.getMessage()); + } + + @ExceptionHandler(InvalidAcquisitionStateException.class) + public ResponseEntity> handleInvalidAcquisitionState(InvalidAcquisitionStateException ex) { + log.error("유효하지 않은 매입 상태: {}", ex.getMessage()); + return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationErrors(MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach(error -> { + String fieldName = ((FieldError) error).getField(); + errors.put(fieldName, error.getDefaultMessage()); + }); + log.error("검증 오류: {}", errors); + Map response = new HashMap<>(); + response.put("timestamp", LocalDateTime.now()); + response.put("status", HttpStatus.BAD_REQUEST.value()); + response.put("error", "Validation Failed"); + response.put("errors", errors); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGenericException(Exception ex) { + log.error("예상치 못한 오류 발생", ex); + return buildErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "예상치 못한 오류가 발생했습니다"); + } + + private ResponseEntity> buildErrorResponse(HttpStatus status, String message) { + Map response = new HashMap<>(); + response.put("timestamp", LocalDateTime.now()); + response.put("status", status.value()); + response.put("error", status.getReasonPhrase()); + response.put("message", message); + return ResponseEntity.status(status).body(response); + } +} + +/** 매입 정보를 찾을 수 없을 때 발생하는 예외 */ +class AcquisitionNotFoundException extends RuntimeException { + public AcquisitionNotFoundException(String message) { super(message); } +} + +/** 매입 상태가 유효하지 않을 때 발생하는 예외 */ +class InvalidAcquisitionStateException extends RuntimeException { + public InvalidAcquisitionStateException(String message) { super(message); } +} \ No newline at end of file From 819c6e64ea9d937142f2c4def901753441435d1c Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 09:39:28 +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.yml | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/main/resources/application.yml diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..e45fc93 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,38 @@ +spring: + application: + name: proj-acquirex + datasource: + url: jdbc:h2:mem:acquirex;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.H2Dialect + h2: + console: + enabled: true + path: /h2-console + +server: + port: 8080 + +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: when_authorized + +logging: + level: + com.acquirex: DEBUG + org.springframework.web: INFO + org.hibernate.SQL: DEBUG \ No newline at end of file From 059df148fb92c45b086d0d09124109673f856c39 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 09:39:29 +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 --- .../com/acquirex/AcquisitionServiceTest.java | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/test/java/com/acquirex/AcquisitionServiceTest.java diff --git a/src/test/java/com/acquirex/AcquisitionServiceTest.java b/src/test/java/com/acquirex/AcquisitionServiceTest.java new file mode 100644 index 0000000..f52fd7b --- /dev/null +++ b/src/test/java/com/acquirex/AcquisitionServiceTest.java @@ -0,0 +1,160 @@ +package com.acquirex; + +import com.acquirex.domain.Acquisition; +import com.acquirex.domain.AcquisitionStatus; +import com.acquirex.exception.AcquisitionNotFoundException; +import com.acquirex.exception.InvalidAcquisitionStateException; +import com.acquirex.service.AcquisitionRequest; +import com.acquirex.service.AcquisitionResponse; +import com.acquirex.service.AcquisitionService; +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 java.time.LocalDateTime; +import java.util.Arrays; +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.*; + +/** + * AcquisitionService 단위 테스트 - Spring Boot 3 마이그레이션 검증 + */ +@ExtendWith(MockitoExtension.class) +class AcquisitionServiceTest { + + @Mock + private com.acquirex.service.AcquisitionRepository acquisitionRepository; + + private AcquisitionService acquisitionService; + private AcquisitionRequest validRequest; + private Acquisition pendingAcquisition; + + @BeforeEach + void setUp() { + acquisitionService = new AcquisitionService(acquisitionRepository); + validRequest = AcquisitionRequest.builder() + .merchantId("MERCHANT001") + .cardNumber("1234567890123456") + .amount(new BigDecimal("50000.00")) + .build(); + + pendingAcquisition = Acquisition.builder() + .id(1L).merchantId("MERCHANT001").cardNumber("123456****3456") + .amount(new BigDecimal("50000.00")).transactionId("TXN123456789") + .status(AcquisitionStatus.PENDING).createdAt(LocalDateTime.now()).build(); + } + + @Test + @DisplayName("매입 요청 등록 성공 - Spring Boot 3 Jakarta Validation") + void registerAcquisition_Success() { + when(acquisitionRepository.save(any(Acquisition.class))).thenAnswer(invocation -> { + Acquisition saved = invocation.getArgument(0); + saved.setId(1L); + saved.setCreatedAt(LocalDateTime.now()); + return saved; + }); + + AcquisitionResponse response = acquisitionService.registerAcquisition(validRequest); + + assertThat(response).isNotNull(); + assertThat(response.getMerchantId()).isEqualTo("MERCHANT001"); + assertThat(response.getAmount()).isEqualByComparingTo(new BigDecimal("50000.00")); + assertThat(response.getStatus()).isEqualTo(AcquisitionStatus.PENDING); + assertThat(response.getCardNumber()).isEqualTo("123456****3456"); + verify(acquisitionRepository, times(1)).save(any(Acquisition.class)); + } + + @Test + @DisplayName("매입 승인 처리 성공 - Spring Boot 3 @Transactional") + void approveAcquisition_Success() { + when(acquisitionRepository.findByTransactionId("TXN123456789")).thenReturn(Optional.of(pendingAcquisition)); + when(acquisitionRepository.save(any(Acquisition.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + AcquisitionResponse response = acquisitionService.approveAcquisition("TXN123456789"); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(AcquisitionStatus.APPROVED); + assertThat(response.getApprovalCode()).isNotNull(); + assertThat(response.getApprovalCode()).hasSize(6); + } + + @Test + @DisplayName("매입 승인 실패 - 매입 정보 없음") + void approveAcquisition_NotFound() { + when(acquisitionRepository.findByTransactionId("INVALID_TXN")).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> acquisitionService.approveAcquisition("INVALID_TXN")) + .isInstanceOf(AcquisitionNotFoundException.class) + .hasMessageContaining("INVALID_TXN"); + } + + @Test + @DisplayName("매입 승인 실패 - 이미 승인된 매입") + void approveAcquisition_AlreadyApproved() { + pendingAcquisition.setStatus(AcquisitionStatus.APPROVED); + when(acquisitionRepository.findByTransactionId("TXN123456789")).thenReturn(Optional.of(pendingAcquisition)); + + assertThatThrownBy(() -> acquisitionService.approveAcquisition("TXN123456789")) + .isInstanceOf(InvalidAcquisitionStateException.class) + .hasMessageContaining("APPROVED"); + } + + @Test + @DisplayName("매입 거절 처리 성공") + void rejectAcquisition_Success() { + when(acquisitionRepository.findByTransactionId("TXN123456789")).thenReturn(Optional.of(pendingAcquisition)); + when(acquisitionRepository.save(any(Acquisition.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + AcquisitionResponse response = acquisitionService.rejectAcquisition("TXN123456789", "한도 초과"); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(AcquisitionStatus.REJECTED); + } + + @Test + @DisplayName("매입 정보 조회 성공") + void getAcquisition_Success() { + when(acquisitionRepository.findByTransactionId("TXN123456789")).thenReturn(Optional.of(pendingAcquisition)); + + AcquisitionResponse response = acquisitionService.getAcquisition("TXN123456789"); + + assertThat(response).isNotNull(); + assertThat(response.getTransactionId()).isEqualTo("TXN123456789"); + } + + @Test + @DisplayName("가맹점별 매입 목록 조회 성공") + void getAcquisitionsByMerchant_Success() { + Acquisition acquisition2 = Acquisition.builder() + .id(2L).merchantId("MERCHANT001").cardNumber("987654****3210") + .amount(new BigDecimal("30000.00")).transactionId("TXN987654321") + .status(AcquisitionStatus.PENDING).createdAt(LocalDateTime.now()).build(); + + when(acquisitionRepository.findByMerchantId("MERCHANT001")).thenReturn(Arrays.asList(pendingAcquisition, acquisition2)); + + List responses = acquisitionService.getAcquisitionsByMerchant("MERCHANT001"); + + assertThat(responses).hasSize(2); + assertThat(responses).extracting(AcquisitionResponse::getMerchantId).containsOnly("MERCHANT001"); + } + + @Test + @DisplayName("대기 중인 매입 목록 조회 성공") + void getPendingAcquisitions_Success() { + when(acquisitionRepository.findByStatus(AcquisitionStatus.PENDING)).thenReturn(Arrays.asList(pendingAcquisition)); + + List responses = acquisitionService.getPendingAcquisitions(); + + assertThat(responses).hasSize(1); + assertThat(responses.get(0).getStatus()).isEqualTo(AcquisitionStatus.PENDING); + } +} \ No newline at end of file