From 3f0712b3cc086be53d884a40497b1aff99a39761 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 12:16:36 +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..b2f83c8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + + com.acquirex + card-acquisition + 1.0.0-SNAPSHOT + card-acquisition + 카드 매입 승인 경로 - 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 + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file From 6b563266381c4040d91bffae4a47412159e024ac Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 12:16:37 +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 --- .../cardacquisition/CardAcquisitionApplication.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/com/acquirex/cardacquisition/CardAcquisitionApplication.java diff --git a/src/main/java/com/acquirex/cardacquisition/CardAcquisitionApplication.java b/src/main/java/com/acquirex/cardacquisition/CardAcquisitionApplication.java new file mode 100644 index 0000000..a262683 --- /dev/null +++ b/src/main/java/com/acquirex/cardacquisition/CardAcquisitionApplication.java @@ -0,0 +1,12 @@ +package com.acquirex.cardacquisition; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CardAcquisitionApplication { + + public static void main(String[] args) { + SpringApplication.run(CardAcquisitionApplication.class, args); + } +} \ No newline at end of file From ead3a4f1e46bca430327f9a1bc42f9bb1d0c26fa Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 12:16:38 +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 --- .../entity/CardAcquisition.java | 183 ++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 src/main/java/com/acquirex/cardacquisition/entity/CardAcquisition.java diff --git a/src/main/java/com/acquirex/cardacquisition/entity/CardAcquisition.java b/src/main/java/com/acquirex/cardacquisition/entity/CardAcquisition.java new file mode 100644 index 0000000..c50e373 --- /dev/null +++ b/src/main/java/com/acquirex/cardacquisition/entity/CardAcquisition.java @@ -0,0 +1,183 @@ +package com.acquirex.cardacquisition.entity; + +import jakarta.persistence.*; +import jakarta.validation.constraints.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** Spring Boot 3 전환: javax.persistence → jakarta.persistence */ +@Entity +@Table(name = "card_acquisitions") +public class CardAcquisition { + + @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 AcquisitionStatus status = AcquisitionStatus.PENDING; + + @Column(name = "approval_code") + private String approvalCode; + + @Column(name = "request_time", nullable = false) + private LocalDateTime requestTime = LocalDateTime.now(); + + @Column(name = "approval_time") + private LocalDateTime approvalTime; + + @Column(name = "reject_reason") + private String rejectReason; + + public CardAcquisition() {} + + public CardAcquisition(String merchantId, String cardNumber, BigDecimal amount, String currency) { + this.merchantId = merchantId; + this.cardNumber = cardNumber; + this.amount = amount; + this.currency = currency; + this.status = AcquisitionStatus.PENDING; + this.requestTime = 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 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 AcquisitionStatus getStatus() { return status; } + public void setStatus(AcquisitionStatus status) { this.status = status; } + public String getApprovalCode() { return approvalCode; } + public void setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; } + public LocalDateTime getRequestTime() { return requestTime; } + public void setRequestTime(LocalDateTime requestTime) { this.requestTime = requestTime; } + public LocalDateTime getApprovalTime() { return approvalTime; } + public void setApprovalTime(LocalDateTime approvalTime) { this.approvalTime = approvalTime; } + public String getRejectReason() { return rejectReason; } + public void setRejectReason(String rejectReason) { this.rejectReason = rejectReason; } + + public void approve(String approvalCode) { + this.status = AcquisitionStatus.APPROVED; + this.approvalCode = approvalCode; + this.approvalTime = LocalDateTime.now(); + } + + public void reject(String reason) { + this.status = AcquisitionStatus.REJECTED; + this.rejectReason = reason; + this.approvalTime = LocalDateTime.now(); + } + + /** Spring Boot 3 전환: javax.persistence → jakarta.persistence */ + public enum AcquisitionStatus { + PENDING, APPROVED, REJECTED, CANCELLED + } +} + +/** Spring Boot 3 전환: javax.validation → jakarta.validation */ +class AcquisitionRequest { + @NotBlank(message = "상점 ID는 필수입니다") + private String merchantId; + + @NotBlank(message = "카드번호는 필수입니다") + @Pattern(regexp = "\\d{10,19}", message = "유효하지 않은 카드번호 형식입니다") + private String cardNumber; + + @NotNull(message = "금액은 필수입니다") + @DecimalMin(value = "0.01", message = "금액은 0보다 커야 합니다") + private BigDecimal amount; + + @NotBlank(message = "통화는 필수입니다") + @Size(min = 3, max = 3, message = "통화 코드는 3자리입니다") + private String currency; + + public AcquisitionRequest() {} + public AcquisitionRequest(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 AcquisitionResponse { + private Long id; + private String merchantId; + private String maskedCardNumber; + private BigDecimal amount; + private String currency; + private CardAcquisition.AcquisitionStatus status; + private String approvalCode; + private LocalDateTime requestTime; + private LocalDateTime approvalTime; + private String message; + + public AcquisitionResponse() {} + + public static AcquisitionResponse fromEntity(CardAcquisition entity) { + AcquisitionResponse response = new AcquisitionResponse(); + response.setId(entity.getId()); + response.setMerchantId(entity.getMerchantId()); + response.setMaskedCardNumber(maskCardNumber(entity.getCardNumber())); + response.setAmount(entity.getAmount()); + response.setCurrency(entity.getCurrency()); + response.setStatus(entity.getStatus()); + response.setApprovalCode(entity.getApprovalCode()); + response.setRequestTime(entity.getRequestTime()); + response.setApprovalTime(entity.getApprovalTime()); + return response; + } + + private static String maskCardNumber(String cardNumber) { + if (cardNumber == null || cardNumber.length() < 8) return "****"; + return cardNumber.substring(0, 4) + "****" + cardNumber.substring(cardNumber.length() - 4); + } + + 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 getMaskedCardNumber() { return maskedCardNumber; } + public void setMaskedCardNumber(String maskedCardNumber) { this.maskedCardNumber = maskedCardNumber; } + 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 CardAcquisition.AcquisitionStatus getStatus() { return status; } + public void setStatus(CardAcquisition.AcquisitionStatus status) { this.status = status; } + public String getApprovalCode() { return approvalCode; } + public void setApprovalCode(String approvalCode) { this.approvalCode = approvalCode; } + public LocalDateTime getRequestTime() { return requestTime; } + public void setRequestTime(LocalDateTime requestTime) { this.requestTime = requestTime; } + public LocalDateTime getApprovalTime() { return approvalTime; } + public void setApprovalTime(LocalDateTime approvalTime) { this.approvalTime = approvalTime; } + public String getMessage() { return message; } + public void setMessage(String message) { this.message = message; } +} \ No newline at end of file From e56f14722c543d01ccef45dbb781fdbbd5cd71a0 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 12:16:40 +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 --- .../repository/CardAcquisitionRepository.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/com/acquirex/cardacquisition/repository/CardAcquisitionRepository.java diff --git a/src/main/java/com/acquirex/cardacquisition/repository/CardAcquisitionRepository.java b/src/main/java/com/acquirex/cardacquisition/repository/CardAcquisitionRepository.java new file mode 100644 index 0000000..75b34af --- /dev/null +++ b/src/main/java/com/acquirex/cardacquisition/repository/CardAcquisitionRepository.java @@ -0,0 +1,23 @@ +package com.acquirex.cardacquisition.repository; + +import com.acquirex.cardacquisition.entity.CardAcquisition; +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 CardAcquisitionRepository extends JpaRepository { + + List findByMerchantId(String merchantId); + + List findByStatus(CardAcquisition.AcquisitionStatus status); + + Optional findByApprovalCode(String approvalCode); + + List findByRequestTimeBetween(LocalDateTime start, LocalDateTime end); + + List findByMerchantIdAndStatus(String merchantId, CardAcquisition.AcquisitionStatus status); +} \ No newline at end of file From 0d4a244b56d4a2b9b04292ec83eb7730d25ed978 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 12:16:41 +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/CardAcquisitionService.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/main/java/com/acquirex/cardacquisition/service/CardAcquisitionService.java diff --git a/src/main/java/com/acquirex/cardacquisition/service/CardAcquisitionService.java b/src/main/java/com/acquirex/cardacquisition/service/CardAcquisitionService.java new file mode 100644 index 0000000..f538519 --- /dev/null +++ b/src/main/java/com/acquirex/cardacquisition/service/CardAcquisitionService.java @@ -0,0 +1,106 @@ +package com.acquirex.cardacquisition.service; + +import com.acquirex.cardacquisition.entity.CardAcquisition; +import com.acquirex.cardacquisition.repository.CardAcquisitionRepository; +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; + +@Service +public class CardAcquisitionService { + + private static final BigDecimal MAX_SINGLE_TRANSACTION = new BigDecimal("10000000"); + private static final BigDecimal DAILY_LIMIT = new BigDecimal("50000000"); + + private final CardAcquisitionRepository repository; + + public CardAcquisitionService(CardAcquisitionRepository repository) { + this.repository = repository; + } + + @Transactional + public AcquisitionResponse requestAcquisition(AcquisitionRequest request) { + validateRequest(request); + + CardAcquisition acquisition = new CardAcquisition( + request.getMerchantId(), request.getCardNumber(), request.getAmount(), request.getCurrency()); + + CardAcquisition saved = repository.save(acquisition); + String approvalCode = generateApprovalCode(); + saved.approve(approvalCode); + + AcquisitionResponse response = AcquisitionResponse.fromEntity(repository.save(saved)); + response.setMessage("매입 승인이 완료되었습니다"); + return response; + } + + @Transactional(readOnly = true) + public AcquisitionResponse getAcquisition(Long id) { + CardAcquisition acquisition = repository.findById(id) + .orElseThrow(() -> new AcquisitionNotFoundException("매입 정보를 찾을 수 없습니다: " + id)); + AcquisitionResponse response = AcquisitionResponse.fromEntity(acquisition); + response.setMessage("매입 정보를 조회했습니다"); + return response; + } + + @Transactional(readOnly = true) + public List getAcquisitionsByMerchant(String merchantId) { + return repository.findByMerchantId(merchantId).stream() + .map(AcquisitionResponse::fromEntity).collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public List getPendingAcquisitions() { + return repository.findByStatus(CardAcquisition.AcquisitionStatus.PENDING).stream() + .map(AcquisitionResponse::fromEntity).collect(Collectors.toList()); + } + + @Transactional + public AcquisitionResponse cancelAcquisition(Long id) { + CardAcquisition acquisition = repository.findById(id) + .orElseThrow(() -> new AcquisitionNotFoundException("매입 정보를 찾을 수 없습니다: " + id)); + + if (acquisition.getStatus() == CardAcquisition.AcquisitionStatus.CANCELLED) { + throw new IllegalStateException("이미 취소된 매입입니다"); + } + + acquisition.setStatus(CardAcquisition.AcquisitionStatus.CANCELLED); + acquisition.setApprovalTime(LocalDateTime.now()); + + AcquisitionResponse response = AcquisitionResponse.fromEntity(repository.save(acquisition)); + response.setMessage("매입이 취소되었습니다"); + return response; + } + + private void validateRequest(AcquisitionRequest request) { + if (request.getAmount().compareTo(MAX_SINGLE_TRANSACTION) > 0) { + throw new InvalidAcquisitionException("단일 거래 한도를 초과했습니다"); + } + + LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay(); + BigDecimal todayTotal = repository.findByRequestTimeBetween(startOfDay, startOfDay.plusDays(1)).stream() + .filter(a -> a.getMerchantId().equals(request.getMerchantId())) + .map(CardAcquisition::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add); + + if (todayTotal.add(request.getAmount()).compareTo(DAILY_LIMIT) > 0) { + throw new InvalidAcquisitionException("일일 한도를 초과했습니다"); + } + } + + private String generateApprovalCode() { + return "APR" + UUID.randomUUID().toString().substring(0, 12).toUpperCase(); + } + + public static class AcquisitionNotFoundException extends RuntimeException { + public AcquisitionNotFoundException(String message) { super(message); } + } + + public static class InvalidAcquisitionException extends RuntimeException { + public InvalidAcquisitionException(String message) { super(message); } + } +} \ No newline at end of file From 05dcb24cb3dcabe490d7125daae2167c2c563a29 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 12:16:42 +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 --- .../controller/CardAcquisitionController.java | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/main/java/com/acquirex/cardacquisition/controller/CardAcquisitionController.java diff --git a/src/main/java/com/acquirex/cardacquisition/controller/CardAcquisitionController.java b/src/main/java/com/acquirex/cardacquisition/controller/CardAcquisitionController.java new file mode 100644 index 0000000..6811d89 --- /dev/null +++ b/src/main/java/com/acquirex/cardacquisition/controller/CardAcquisitionController.java @@ -0,0 +1,91 @@ +package com.acquirex.cardacquisition.controller; + +import com.acquirex.cardacquisition.dto.AcquisitionRequest; +import com.acquirex.cardacquisition.dto.AcquisitionResponse; +import com.acquirex.cardacquisition.service.CardAcquisitionService; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +/** Spring Boot 3 전환: javax.servlet → jakarta.servlet */ +@RestController +@RequestMapping("/api/v1/acquisitions") +public class CardAcquisitionController { + + private final CardAcquisitionService service; + + public CardAcquisitionController(CardAcquisitionService service) { + this.service = service; + } + + @PostMapping + public ResponseEntity requestAcquisition(@Valid @RequestBody AcquisitionRequest request) { + return ResponseEntity.status(HttpStatus.CREATED).body(service.requestAcquisition(request)); + } + + @GetMapping("/{id}") + public ResponseEntity getAcquisition(@PathVariable Long id) { + return ResponseEntity.ok(service.getAcquisition(id)); + } + + @GetMapping("/merchant/{merchantId}") + public ResponseEntity> getAcquisitionsByMerchant(@PathVariable String merchantId) { + return ResponseEntity.ok(service.getAcquisitionsByMerchant(merchantId)); + } + + @GetMapping("/pending") + public ResponseEntity> getPendingAcquisitions() { + return ResponseEntity.ok(service.getPendingAcquisitions()); + } + + @PostMapping("/{id}/cancel") + public ResponseEntity cancelAcquisition(@PathVariable Long id) { + return ResponseEntity.ok(service.cancelAcquisition(id)); + } +} + +/** Spring Boot 3 전환: javax.validation → jakarta.validation */ +@RestControllerAdvice +class GlobalExceptionHandler { + + @ExceptionHandler(CardAcquisitionService.AcquisitionNotFoundException.class) + public ResponseEntity> handleNotFound(CardAcquisitionService.AcquisitionNotFoundException ex) { + return buildErrorResponse(HttpStatus.NOT_FOUND, ex.getMessage()); + } + + @ExceptionHandler(CardAcquisitionService.InvalidAcquisitionException.class) + public ResponseEntity> handleInvalid(CardAcquisitionService.InvalidAcquisitionException ex) { + return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage()); + } + + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity> handleIllegalState(IllegalStateException ex) { + return buildErrorResponse(HttpStatus.CONFLICT, ex.getMessage()); + } + + @ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationErrors( + org.springframework.web.bind.MethodArgumentNotValidException ex) { + java.util.Map fieldErrors = new java.util.HashMap<>(); + ex.getBindingResult().getFieldErrors().forEach(e -> fieldErrors.put(e.getField(), e.getDefaultMessage())); + + java.util.Map body = new java.util.HashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("status", HttpStatus.BAD_REQUEST.value()); + body.put("error", "Validation Failed"); + body.put("fieldErrors", fieldErrors); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(body); + } + + private ResponseEntity> buildErrorResponse(HttpStatus status, String message) { + java.util.Map body = new java.util.HashMap<>(); + body.put("timestamp", LocalDateTime.now()); + body.put("status", status.value()); + body.put("error", status.getReasonPhrase()); + body.put("message", message); + return ResponseEntity.status(status).body(body); + } +} \ No newline at end of file From 91e77cb666e21dd77949fc6c40fd11e44f2f391e Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 12:16:44 +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 | 24 +++++++++++++++++++++++ 1 file changed, 24 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..920b6a5 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,24 @@ +spring.application.name=card-acquisition + +# H2 Database +spring.datasource.url=jdbc:h2:mem:cardacquisition;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +# JPA/Hibernate 6 (Spring Boot 3) +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 +server.port=8080 + +# Jackson +spring.jackson.serialization.write-dates-as-timestamps=false +spring.jackson.date-format=yyyy-MM-dd'T'HH:mm:ss \ No newline at end of file From 5461d752915b3fe4aef5ffe69c3380002914a731 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 12:16:45 +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 --- .../CardAcquisitionServiceTest.java | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 src/test/java/com/acquirex/cardacquisition/CardAcquisitionServiceTest.java diff --git a/src/test/java/com/acquirex/cardacquisition/CardAcquisitionServiceTest.java b/src/test/java/com/acquirex/cardacquisition/CardAcquisitionServiceTest.java new file mode 100644 index 0000000..57c3151 --- /dev/null +++ b/src/test/java/com/acquirex/cardacquisition/CardAcquisitionServiceTest.java @@ -0,0 +1,98 @@ +package com.acquirex.cardacquisition; + +import com.acquirex.cardacquisition.entity.CardAcquisition; +import com.acquirex.cardacquisition.repository.CardAcquisitionRepository; +import com.acquirex.cardacquisition.service.CardAcquisitionService; +import org.junit.jupiter.api.BeforeEach; +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.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 CardAcquisitionServiceTest { + + @Mock + private CardAcquisitionRepository repository; + + private CardAcquisitionService service; + + @BeforeEach + void setUp() { + service = new CardAcquisitionService(repository); + } + + @Test + void requestAcquisition_ValidRequest_ReturnsApprovedResponse() { + when(repository.save(any(CardAcquisition.class))).thenAnswer(i -> { + CardAcquisition e = i.getArgument(0); + if (e.getId() == null) e.setId(1L); + return e; + }); + when(repository.findByRequestTimeBetween(any(), any())).thenReturn(List.of()); + + AcquisitionRequest request = new AcquisitionRequest("MERCHANT001", "1234567890123456", new BigDecimal("50000"), "KRW"); + AcquisitionResponse response = service.requestAcquisition(request); + + assertNotNull(response); + assertEquals(1L, response.getId()); + assertEquals(CardAcquisition.AcquisitionStatus.APPROVED, response.getStatus()); + assertTrue(response.getApprovalCode().startsWith("APR")); + verify(repository, times(2)).save(any(CardAcquisition.class)); + } + + @Test + void requestAcquisition_ExceedsLimit_ThrowsException() { + AcquisitionRequest request = new AcquisitionRequest("MERCHANT001", "1234567890123456", new BigDecimal("15000000"), "KRW"); + assertThrows(CardAcquisitionService.InvalidAcquisitionException.class, () -> service.requestAcquisition(request)); + } + + @Test + void getAcquisition_ExistingId_ReturnsResponse() { + CardAcquisition entity = new CardAcquisition("M1", "1111", new BigDecimal("1000"), "KRW"); + entity.setId(1L); + entity.approve("APR123"); + when(repository.findById(1L)).thenReturn(Optional.of(entity)); + + AcquisitionResponse response = service.getAcquisition(1L); + + assertNotNull(response); + assertEquals(CardAcquisition.AcquisitionStatus.APPROVED, response.getStatus()); + } + + @Test + void getAcquisition_NotFound_ThrowsException() { + when(repository.findById(999L)).thenReturn(Optional.empty()); + assertThrows(CardAcquisitionService.AcquisitionNotFoundException.class, () -> service.getAcquisition(999L)); + } + + @Test + void cancelAcquisition_ExistingPending_ReturnsCancelled() { + CardAcquisition entity = new CardAcquisition("M1", "1111", new BigDecimal("1000"), "KRW"); + entity.setId(1L); + when(repository.findById(1L)).thenReturn(Optional.of(entity)); + when(repository.save(any())).thenAnswer(i -> i.getArgument(0)); + + AcquisitionResponse response = service.cancelAcquisition(1L); + + assertEquals(CardAcquisition.AcquisitionStatus.CANCELLED, response.getStatus()); + } + + @Test + void cancelAcquisition_AlreadyCancelled_ThrowsException() { + CardAcquisition entity = new CardAcquisition("M1", "1111", new BigDecimal("1000"), "KRW"); + entity.setId(1L); + entity.setStatus(CardAcquisition.AcquisitionStatus.CANCELLED); + when(repository.findById(1L)).thenReturn(Optional.of(entity)); + + assertThrows(IllegalStateException.class, () -> service.cancelAcquisition(1L)); + } +} \ No newline at end of file