From fc2e4d049c89ef5823bbc1b7b1f98bd6ee2536aa Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 09:39:24 +0000 Subject: [PATCH] =?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