From b3f0574fc17b1631399556f57628b5ccf82732e0 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Thu, 30 Jul 2026 10:38:09 +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 --- .../service/AcquisitionApprovalService.java | 215 ++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 src/main/java/com/acquirex/cardapproval/service/AcquisitionApprovalService.java diff --git a/src/main/java/com/acquirex/cardapproval/service/AcquisitionApprovalService.java b/src/main/java/com/acquirex/cardapproval/service/AcquisitionApprovalService.java new file mode 100644 index 0000000..edc8503 --- /dev/null +++ b/src/main/java/com/acquirex/cardapproval/service/AcquisitionApprovalService.java @@ -0,0 +1,215 @@ +package com.acquirex.cardapproval.service; + +import com.acquirex.cardapproval.domain.AcquisitionApproval; +import com.acquirex.cardapproval.domain.AcquisitionApproval.ApprovalStatus; +import com.acquirex.cardapproval.domain.AcquisitionApprovalRepository; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +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.*; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 카드 매입 승인 서비스 + * Spring Boot 3.x 트랜잭션 관리 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class AcquisitionApprovalService { + + private final AcquisitionApprovalRepository repository; + + @Transactional + public AcquisitionApprovalResponse processAcquisition(AcquisitionApprovalRequest request) { + log.info("매입 승인 요청 처리: 가맹점={}, 금액={}", request.getMerchantId(), request.getAcquisitionAmount()); + + repository.findByApprovalNumber(request.getApprovalNumber()) + .ifPresent(existing -> { + throw new AcquisitionApprovalException("DUPLICATE_APPROVAL", "이미 처리된 승인번호입니다"); + }); + + if (request.getAcquisitionAmount().compareTo(request.getApprovalAmount()) != 0) { + throw new AcquisitionApprovalException("AMOUNT_MISMATCH", "매입 금액과 승인 금액이 일치하지 않습니다"); + } + + AcquisitionApproval approval = AcquisitionApproval.builder() + .merchantId(request.getMerchantId()) + .cardNumber(request.getCardNumber()) + .acquisitionAmount(request.getAcquisitionAmount()) + .approvalAmount(request.getApprovalAmount()) + .approvalNumber(request.getApprovalNumber()) + .acquisitionDatetime(request.getAcquisitionDatetime()) + .approvalStatus(ApprovalStatus.APPROVED) + .cancelYn("N") + .build(); + + AcquisitionApproval saved = repository.save(approval); + log.info("매입 승인 완료: ID={}, 상태={}", saved.getId(), saved.getApprovalStatus()); + return mapToResponse(saved); + } + + @Transactional + public AcquisitionApprovalResponse cancelAcquisition(Long id, String reason) { + log.info("매입 취소 요청: ID={}, 사유={}", id, reason); + + AcquisitionApproval approval = repository.findById(id) + .orElseThrow(() -> new AcquisitionApprovalException("NOT_FOUND", "매입 승인건을 찾을 수 없습니다: " + id)); + + if ("Y".equals(approval.getCancelYn())) { + throw new AcquisitionApprovalException("ALREADY_CANCELLED", "이미 취소된 건입니다"); + } + + approval.cancel(reason); + AcquisitionApproval saved = repository.save(approval); + log.info("매입 취소 완료: ID={}", saved.getId()); + return mapToResponse(saved); + } + + @Transactional(readOnly = true) + public AcquisitionApprovalResponse getAcquisition(Long id) { + return repository.findById(id) + .map(this::mapToResponse) + .orElseThrow(() -> new AcquisitionApprovalException("NOT_FOUND", "매입 승인건을 찾을 수 없습니다: " + id)); + } + + @Transactional(readOnly = true) + public List getAcquisitionsByMerchant(String merchantId) { + return repository.findByMerchantIdAndCancelYn(merchantId, "N") + .stream().map(this::mapToResponse).collect(Collectors.toList()); + } + + @Transactional(readOnly = true) + public List getAcquisitionsByPeriod(LocalDateTime startDate, LocalDateTime endDate) { + return repository.findByAcquisitionDatetimeBetween(startDate, endDate) + .stream().map(this::mapToResponse).collect(Collectors.toList()); + } + + private AcquisitionApprovalResponse mapToResponse(AcquisitionApproval approval) { + return AcquisitionApprovalResponse.builder() + .id(approval.getId()) + .merchantId(approval.getMerchantId()) + .cardNumber(approval.getCardNumber()) + .acquisitionAmount(approval.getAcquisitionAmount()) + .approvalAmount(approval.getApprovalAmount()) + .approvalNumber(approval.getApprovalNumber()) + .acquisitionDatetime(approval.getAcquisitionDatetime()) + .approvalStatus(approval.getApprovalStatus()) + .cancelYn(approval.getCancelYn()) + .cancelDatetime(approval.getCancelDatetime()) + .cancelReason(approval.getCancelReason()) + .createdAt(approval.getCreatedAt()) + .updatedAt(approval.getUpdatedAt()) + .build(); + } + + // DTOs + @Data @NoArgsConstructor @AllArgsConstructor @Builder + public static class AcquisitionApprovalRequest { + @NotBlank(message = "가맹점 번호는 필수입니다") + private String merchantId; + @NotBlank(message = "카드번호는 필수입니다") + private String cardNumber; + @NotNull @Positive + private BigDecimal acquisitionAmount; + @NotNull @Positive + private BigDecimal approvalAmount; + @NotBlank + private String approvalNumber; + @NotNull + private LocalDateTime acquisitionDatetime; + } + + @Data @NoArgsConstructor @AllArgsConstructor @Builder + public static class AcquisitionApprovalResponse { + private Long id; + private String merchantId; + private String cardNumber; + private BigDecimal acquisitionAmount; + private BigDecimal approvalAmount; + private String approvalNumber; + private LocalDateTime acquisitionDatetime; + private ApprovalStatus approvalStatus; + private String cancelYn; + private LocalDateTime cancelDatetime; + private String cancelReason; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + } + + // Exception + public static class AcquisitionApprovalException extends RuntimeException { + private final String errorCode; + public AcquisitionApprovalException(String errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + } + + // Global Exception Handler + @RestControllerAdvice + @Slf4j + public static class GlobalExceptionHandler { + @ExceptionHandler(AcquisitionApprovalException.class) + public ResponseEntity> handleAcquisitionApprovalException(AcquisitionApprovalException ex) { + log.error("매입 승인 예외: 코드={}, 메시지={}", ex.getErrorCode(), ex.getMessage()); + HttpStatus status = switch (ex.getErrorCode()) { + case "NOT_FOUND" -> HttpStatus.NOT_FOUND; + case "DUPLICATE_APPROVAL", "ALREADY_CANCELLED" -> HttpStatus.CONFLICT; + case "AMOUNT_MISMATCH" -> HttpStatus.BAD_REQUEST; + default -> HttpStatus.INTERNAL_SERVER_ERROR; + }; + Map response = new HashMap<>(); + response.put("timestamp", LocalDateTime.now()); + response.put("status", status.value()); + response.put("errorCode", ex.getErrorCode()); + response.put("message", ex.getMessage()); + return ResponseEntity.status(status).body(response); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationException(MethodArgumentNotValidException ex) { + Map fieldErrors = new HashMap<>(); + for (FieldError error : ex.getBindingResult().getFieldErrors()) { + fieldErrors.put(error.getField(), error.getDefaultMessage()); + } + Map response = new HashMap<>(); + response.put("timestamp", LocalDateTime.now()); + response.put("status", HttpStatus.BAD_REQUEST.value()); + response.put("errorCode", "VALIDATION_ERROR"); + response.put("message", "입력 검증에 실패했습니다"); + response.put("fieldErrors", fieldErrors); + return ResponseEntity.badRequest().body(response); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGenericException(Exception ex) { + log.error("예상치 못한 예외 발생", ex); + Map response = new HashMap<>(); + response.put("timestamp", LocalDateTime.now()); + response.put("status", HttpStatus.INTERNAL_SERVER_ERROR.value()); + response.put("errorCode", "INTERNAL_ERROR"); + response.put("message", "서버 내부 오류가 발생했습니다"); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } +} \ No newline at end of file