From 0f821becab32a792036a636cbfc59bb25f320d4c Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 13:00: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 --- proj-acquirex/pom.xml | 79 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 proj-acquirex/pom.xml diff --git a/proj-acquirex/pom.xml b/proj-acquirex/pom.xml new file mode 100644 index 0000000..c86047a --- /dev/null +++ b/proj-acquirex/pom.xml @@ -0,0 +1,79 @@ + + + 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 Migration + + + 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.projectlombok + lombok + true + + + + + + + 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 f0650c45404b9836072fb9964560bc6eeb5f61ba Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 13:00: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 --- .../main/java/com/acquirex/AcquirexApplication.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 proj-acquirex/src/main/java/com/acquirex/AcquirexApplication.java 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 From b7f1481968fa05d04da7d81335f5f033ff5dff5f Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 13:00: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 --- .../approval/domain/AcquisitionApproval.java | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 proj-acquirex/src/main/java/com/acquirex/approval/domain/AcquisitionApproval.java diff --git a/proj-acquirex/src/main/java/com/acquirex/approval/domain/AcquisitionApproval.java b/proj-acquirex/src/main/java/com/acquirex/approval/domain/AcquisitionApproval.java new file mode 100644 index 0000000..a4cf223 --- /dev/null +++ b/proj-acquirex/src/main/java/com/acquirex/approval/domain/AcquisitionApproval.java @@ -0,0 +1,86 @@ +package com.acquirex.approval.domain; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 카드 매입 승인 도메인 엔티티 + * Spring Boot 3: javax.* → jakarta.* 마이그레이션 + */ +@Entity +@Table(name = "acquisition_approval") +public class AcquisitionApproval { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "merchant_id", nullable = false, length = 20) + private String merchantId; + + @Column(name = "card_number", nullable = false, length = 19) + private String cardNumber; + + @Column(name = "approval_amount", nullable = false, precision = 15, scale = 2) + private BigDecimal approvalAmount; + + @Column(name = "approval_number", nullable = false, length = 12) + private String approvalNumber; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false, length = 20) + private ApprovalStatus status; + + @Column(name = "approval_datetime", nullable = false) + private LocalDateTime approvalDatetime; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + public AcquisitionApproval() {} + + public AcquisitionApproval(String merchantId, String cardNumber, BigDecimal approvalAmount, + String approvalNumber, ApprovalStatus status, LocalDateTime approvalDatetime) { + this.merchantId = merchantId; + this.cardNumber = cardNumber; + this.approvalAmount = approvalAmount; + this.approvalNumber = approvalNumber; + this.status = status; + this.approvalDatetime = approvalDatetime; + this.createdAt = LocalDateTime.now(); + } + + 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 getApprovalAmount() { return approvalAmount; } + public void setApprovalAmount(BigDecimal approvalAmount) { this.approvalAmount = approvalAmount; } + public String getApprovalNumber() { return approvalNumber; } + public void setApprovalNumber(String approvalNumber) { this.approvalNumber = approvalNumber; } + public ApprovalStatus getStatus() { return status; } + public void setStatus(ApprovalStatus status) { this.status = status; } + public LocalDateTime getApprovalDatetime() { return approvalDatetime; } + public void setApprovalDatetime(LocalDateTime approvalDatetime) { this.approvalDatetime = approvalDatetime; } + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + public LocalDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } + + @PreUpdate + protected void onUpdate() { this.updatedAt = LocalDateTime.now(); } + + /** 매입 승인 상태 열거형 */ + public enum ApprovalStatus { + PENDING("대기"), APPROVED("승인"), REJECTED("거절"), CANCELLED("취소"); + private final String description; + ApprovalStatus(String description) { this.description = description; } + public String getDescription() { return description; } + } +} \ No newline at end of file From 49155eb293d90a084f0849961801c1212daa9ee4 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 13:00:23 +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 | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 proj-acquirex/src/main/java/com/acquirex/approval/repository/AcquisitionApprovalRepository.java diff --git a/proj-acquirex/src/main/java/com/acquirex/approval/repository/AcquisitionApprovalRepository.java b/proj-acquirex/src/main/java/com/acquirex/approval/repository/AcquisitionApprovalRepository.java new file mode 100644 index 0000000..4391c87 --- /dev/null +++ b/proj-acquirex/src/main/java/com/acquirex/approval/repository/AcquisitionApprovalRepository.java @@ -0,0 +1,18 @@ +package com.acquirex.approval.repository; + +import com.acquirex.approval.domain.AcquisitionApproval; +import com.acquirex.approval.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; + +/** 카드 매입 승인 레포지토리 - Spring Boot 3: Spring Data JPA 3.x 호환 */ +@Repository +public interface AcquisitionApprovalRepository extends JpaRepository { + Optional findByApprovalNumber(String approvalNumber); + List findByMerchantIdAndStatus(String merchantId, ApprovalStatus status); + List findByApprovalDatetimeBetween(LocalDateTime start, LocalDateTime end); +} \ No newline at end of file From 3890fe28fc60ffac24455fb4be3871fe463dc215 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 13:00: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 --- .../service/AcquisitionApprovalService.java | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 proj-acquirex/src/main/java/com/acquirex/approval/service/AcquisitionApprovalService.java diff --git a/proj-acquirex/src/main/java/com/acquirex/approval/service/AcquisitionApprovalService.java b/proj-acquirex/src/main/java/com/acquirex/approval/service/AcquisitionApprovalService.java new file mode 100644 index 0000000..1e41eec --- /dev/null +++ b/proj-acquirex/src/main/java/com/acquirex/approval/service/AcquisitionApprovalService.java @@ -0,0 +1,115 @@ +package com.acquirex.approval.service; + +import com.acquirex.approval.domain.AcquisitionApproval; +import com.acquirex.approval.domain.AcquisitionApproval.ApprovalStatus; +import com.acquirex.approval.repository.AcquisitionApprovalRepository; +import jakarta.validation.constraints.*; +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.UUID; +import java.util.stream.Collectors; + +/** 카드 매입 승인 서비스 - Spring Boot 3 전환 적용 */ +@Service +@Transactional(readOnly = true) +public class AcquisitionApprovalService { + + private final AcquisitionApprovalRepository repository; + + public AcquisitionApprovalService(AcquisitionApprovalRepository repository) { + this.repository = repository; + } + + /** 매입 승인 요청 처리 */ + @Transactional + public ApprovalResponse processApproval(ApprovalRequest request) { + String approvalNumber = generateApprovalNumber(); + AcquisitionApproval approval = new AcquisitionApproval( + request.merchantId, maskCardNumber(request.cardNumber), + request.approvalAmount, approvalNumber, ApprovalStatus.APPROVED, LocalDateTime.now()); + return toResponse(repository.save(approval)); + } + + /** 승인 번호로 조회 */ + public ApprovalResponse findByApprovalNumber(String approvalNumber) { + return repository.findByApprovalNumber(approvalNumber) + .map(this::toResponse) + .orElseThrow(() -> new IllegalArgumentException("승인 번호를 찾을 수 없습니다: " + approvalNumber)); + } + + /** 가맹점별 승인 목록 조회 */ + public List findByMerchantId(String merchantId, ApprovalStatus status) { + return repository.findByMerchantIdAndStatus(merchantId, status) + .stream().map(this::toResponse).collect(Collectors.toList()); + } + + /** 승인 취소 처리 */ + @Transactional + public ApprovalResponse cancelApproval(Long id) { + AcquisitionApproval approval = repository.findById(id) + .orElseThrow(() -> new IllegalArgumentException("승인 정보를 찾을 수 없습니다: " + id)); + if (approval.getStatus() == ApprovalStatus.CANCELLED) { + throw new IllegalStateException("이미 취소된 승인입니다"); + } + approval.setStatus(ApprovalStatus.CANCELLED); + return toResponse(repository.save(approval)); + } + + private String generateApprovalNumber() { + return "AP" + System.currentTimeMillis() + UUID.randomUUID().toString().substring(0, 4).toUpperCase(); + } + + private String maskCardNumber(String cardNumber) { + if (cardNumber == null || cardNumber.length() < 10) return cardNumber; + return cardNumber.substring(0, 6) + "****" + cardNumber.substring(cardNumber.length() - 4); + } + + private ApprovalResponse toResponse(AcquisitionApproval a) { + return new ApprovalResponse(a.getId(), a.getMerchantId(), a.getCardNumber(), + a.getApprovalAmount(), a.getApprovalNumber(), a.getStatus(), a.getApprovalDatetime()); + } + + /** 카드 매입 승인 요청 DTO - Spring Boot 3: jakarta.validation 적용 */ + public static class ApprovalRequest { + @NotBlank(message = "가맹점 ID는 필수입니다") + @Size(max = 20) + public String merchantId; + + @NotBlank(message = "카드번호는 필수입니다") + @Pattern(regexp = "\\d{13,19}", message = "카드번호는 13~19자리 숫자입니다") + public String cardNumber; + + @NotNull(message = "승인 금액은 필수입니다") + @DecimalMin(value = "0.01", message = "승인 금액은 0보다 커야 합니다") + @DecimalMax(value = "999999999.99") + public BigDecimal approvalAmount; + + public ApprovalRequest() {} + public ApprovalRequest(String merchantId, String cardNumber, BigDecimal approvalAmount) { + this.merchantId = merchantId; this.cardNumber = cardNumber; this.approvalAmount = approvalAmount; + } + } + + /** 카드 매입 승인 응답 DTO */ + public static class ApprovalResponse { + public Long id; + public String merchantId; + public String cardNumber; + public BigDecimal approvalAmount; + public String approvalNumber; + public ApprovalStatus status; + public LocalDateTime approvalDatetime; + + public ApprovalResponse() {} + public ApprovalResponse(Long id, String merchantId, String cardNumber, BigDecimal approvalAmount, + String approvalNumber, ApprovalStatus status, LocalDateTime approvalDatetime) { + this.id = id; this.merchantId = merchantId; this.cardNumber = cardNumber; + this.approvalAmount = approvalAmount; this.approvalNumber = approvalNumber; + this.status = status; this.approvalDatetime = approvalDatetime; + } + } +} \ No newline at end of file From b04365350423ff3418c858dbbc2bc6afb87bdea1 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 13:00:26 +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 | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 proj-acquirex/src/main/java/com/acquirex/approval/controller/AcquisitionApprovalController.java diff --git a/proj-acquirex/src/main/java/com/acquirex/approval/controller/AcquisitionApprovalController.java b/proj-acquirex/src/main/java/com/acquirex/approval/controller/AcquisitionApprovalController.java new file mode 100644 index 0000000..a3419e2 --- /dev/null +++ b/proj-acquirex/src/main/java/com/acquirex/approval/controller/AcquisitionApprovalController.java @@ -0,0 +1,61 @@ +package com.acquirex.approval.controller; + +import com.acquirex.approval.domain.AcquisitionApproval.ApprovalStatus; +import com.acquirex.approval.service.AcquisitionApprovalService; +import com.acquirex.approval.service.AcquisitionApprovalService.ApprovalRequest; +import com.acquirex.approval.service.AcquisitionApprovalService.ApprovalResponse; +import jakarta.validation.Valid; +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.* 네임스페이스 적용 */ +@RestController +@RequestMapping("/api/v1/acquisitions") +public class AcquisitionApprovalController { + + private final AcquisitionApprovalService service; + + public AcquisitionApprovalController(AcquisitionApprovalService service) { + this.service = service; + } + + @PostMapping("/approvals") + public ResponseEntity createApproval(@Valid @RequestBody ApprovalRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(service.processApproval(request)); + } + + @GetMapping("/approvals/{approvalNumber}") + public ResponseEntity getApproval(@PathVariable String approvalNumber) { + return ResponseEntity.ok(service.findByApprovalNumber(approvalNumber)); + } + + @GetMapping("/merchants/{merchantId}/approvals") + public ResponseEntity> getApprovalsByMerchant( + @PathVariable String merchantId, @RequestParam(required = false) ApprovalStatus status) { + return ResponseEntity.ok(service.findByMerchantId(merchantId, status)); + } + + @PostMapping("/approvals/{id}/cancel") + public ResponseEntity cancelApproval(@PathVariable Long id) { + return ResponseEntity.ok(service.cancelApproval(id)); + } + + @ExceptionHandler(jakarta.validation.ConstraintViolationException.class) + public ResponseEntity> handleConstraintViolation(jakarta.validation.ConstraintViolationException ex) { + return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage())); + } + + @ExceptionHandler(IllegalArgumentException.class) + public ResponseEntity> handleIllegalArgument(IllegalArgumentException ex) { + return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage())); + } + + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity> handleIllegalState(IllegalStateException ex) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of("error", ex.getMessage())); + } +} \ No newline at end of file From 37e2f81ff93f25227a2e9d97480d8fdfc0daa8d8 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 13:00: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.properties | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 proj-acquirex/src/main/resources/application.properties diff --git a/proj-acquirex/src/main/resources/application.properties b/proj-acquirex/src/main/resources/application.properties new file mode 100644 index 0000000..7b29f77 --- /dev/null +++ b/proj-acquirex/src/main/resources/application.properties @@ -0,0 +1,23 @@ +# Spring Boot 3.x Configuration +spring.application.name=proj-acquirex +server.port=8080 + +# H2 Database Configuration +spring.datasource.url=jdbc:h2:mem:acquirex;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 + +# Jackson Configuration +spring.jackson.serialization.write-dates-as-timestamps=false +spring.jackson.time-zone=Asia/Seoul \ No newline at end of file From 8314c93c2b3188102bf762a1e4096a5c85cb43f4 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 13:00: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 --- .../approval/AcquisitionApprovalTest.java | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 proj-acquirex/src/test/java/com/acquirex/approval/AcquisitionApprovalTest.java diff --git a/proj-acquirex/src/test/java/com/acquirex/approval/AcquisitionApprovalTest.java b/proj-acquirex/src/test/java/com/acquirex/approval/AcquisitionApprovalTest.java new file mode 100644 index 0000000..9155f16 --- /dev/null +++ b/proj-acquirex/src/test/java/com/acquirex/approval/AcquisitionApprovalTest.java @@ -0,0 +1,158 @@ +package com.acquirex.approval; + +import com.acquirex.approval.domain.AcquisitionApproval; +import com.acquirex.approval.domain.AcquisitionApproval.ApprovalStatus; +import com.acquirex.approval.repository.AcquisitionApprovalRepository; +import com.acquirex.approval.service.AcquisitionApprovalService; +import com.acquirex.approval.service.AcquisitionApprovalService.ApprovalRequest; +import com.acquirex.approval.service.AcquisitionApprovalService.ApprovalResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; + +import java.math.BigDecimal; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** 카드 매입 승인 통합 테스트 - Spring Boot 3 전환 검증 */ +@SpringBootTest +@AutoConfigureMockMvc +class AcquisitionApprovalTest { + + @Autowired private MockMvc mockMvc; + @Autowired private AcquisitionApprovalRepository repository; + @Autowired private AcquisitionApprovalService service; + + @BeforeEach + void setUp() { repository.deleteAll(); } + + // === Service Tests === + @Test + @DisplayName("매입 승인 요청 처리 성공") + void processApproval_Success() { + ApprovalRequest request = new ApprovalRequest("MERCH001", "1234567890123456", new BigDecimal("50000.00")); + ApprovalResponse response = service.processApproval(request); + + assertNotNull(response); + assertEquals("MERCH001", response.merchantId); + assertEquals(ApprovalStatus.APPROVED, response.status); + assertTrue(response.approvalNumber.startsWith("AP")); + assertTrue(response.cardNumber.matches("\\d{6}\*\*\*\*\\d{4}")); + } + + @Test + @DisplayName("승인 번호로 조회 성공") + void findByApprovalNumber_Success() { + ApprovalRequest request = new ApprovalRequest("MERCH001", "1234567890123456", new BigDecimal("50000.00")); + ApprovalResponse created = service.processApproval(request); + + ApprovalResponse found = service.findByApprovalNumber(created.approvalNumber); + assertNotNull(found); + assertEquals(created.approvalNumber, found.approvalNumber); + } + + @Test + @DisplayName("승인 번호로 조회 - 존재하지 않는 경우 예외 발생") + void findByApprovalNumber_NotFound() { + assertThrows(IllegalArgumentException.class, () -> service.findByApprovalNumber("NOTEXIST")); + } + + @Test + @DisplayName("승인 취소 성공") + void cancelApproval_Success() { + ApprovalRequest request = new ApprovalRequest("MERCH001", "1234567890123456", new BigDecimal("50000.00")); + ApprovalResponse created = service.processApproval(request); + + ApprovalResponse cancelled = service.cancelApproval(created.id); + assertEquals(ApprovalStatus.CANCELLED, cancelled.status); + } + + @Test + @DisplayName("승인 취소 - 이미 취소된 경우 예외 발생") + void cancelApproval_AlreadyCancelled() { + ApprovalRequest request = new ApprovalRequest("MERCH001", "1234567890123456", new BigDecimal("50000.00")); + ApprovalResponse created = service.processApproval(request); + service.cancelApproval(created.id); + + assertThrows(IllegalStateException.class, () -> service.cancelApproval(created.id)); + } + + // === Controller Tests === + @Test + @DisplayName("POST /api/v1/acquisitions/approvals - 승인 요청 성공") + void createApproval_Success() throws Exception { + String json = "{\"merchantId\":\"MERCH001\",\"cardNumber\":\"1234567890123456\",\"approvalAmount\":50000.00}"; + + mockMvc.perform(post("/api/v1/acquisitions/approvals") + .contentType(MediaType.APPLICATION_JSON).content(json)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.merchantId").value("MERCH001")) + .andExpect(jsonPath("$.status").value("APPROVED")); + } + + @Test + @DisplayName("POST /api/v1/acquisitions/approvals - 유효성 검증 실패") + void createApproval_ValidationError() throws Exception { + String json = "{\"merchantId\":\"\",\"cardNumber\":\"123\",\"approvalAmount\":-100}"; + + mockMvc.perform(post("/api/v1/acquisitions/approvals") + .contentType(MediaType.APPLICATION_JSON).content(json)) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("GET /api/v1/acquisitions/approvals/{approvalNumber} - 승인 조회 성공") + void getApproval_Success() throws Exception { + String json = "{\"merchantId\":\"MERCH001\",\"cardNumber\":\"1234567890123456\",\"approvalAmount\":50000.00}"; + MvcResult result = mockMvc.perform(post("/api/v1/acquisitions/approvals") + .contentType(MediaType.APPLICATION_JSON).content(json)).andReturn(); + + String approvalNumber = com.jayway.jsonpath.JsonPath.read(result.getResponse().getContentAsString(), "$.approvalNumber"); + + mockMvc.perform(get("/api/v1/acquisitions/approvals/{approvalNumber}", approvalNumber)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.approvalNumber").value(approvalNumber)); + } + + @Test + @DisplayName("POST /api/v1/acquisitions/approvals/{id}/cancel - 승인 취소 성공") + void cancelApproval_Controller_Success() throws Exception { + String json = "{\"merchantId\":\"MERCH001\",\"cardNumber\":\"1234567890123456\",\"approvalAmount\":50000.00}"; + MvcResult result = mockMvc.perform(post("/api/v1/acquisitions/approvals") + .contentType(MediaType.APPLICATION_JSON).content(json)).andReturn(); + + Long id = com.jayway.jsonpath.JsonPath.read(result.getResponse().getContentAsString(), "$.id"); + + mockMvc.perform(post("/api/v1/acquisitions/approvals/{id}/cancel", id)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("CANCELLED")); + } + + // === Domain Tests === + @Test + @DisplayName("ApprovalStatus 모든 상태 확인") + void testAllStatuses() { + assertEquals(4, ApprovalStatus.values().length); + assertNotNull(ApprovalStatus.PENDING); + assertNotNull(ApprovalStatus.APPROVED); + assertNotNull(ApprovalStatus.REJECTED); + assertNotNull(ApprovalStatus.CANCELLED); + } + + @Test + @DisplayName("ApprovalStatus 설명 확인") + void testStatusDescriptions() { + assertEquals("대기", ApprovalStatus.PENDING.getDescription()); + assertEquals("승인", ApprovalStatus.APPROVED.getDescription()); + assertEquals("거절", ApprovalStatus.REJECTED.getDescription()); + assertEquals("취소", ApprovalStatus.CANCELLED.getDescription()); + } +} \ No newline at end of file