카드 매입 승인 경로 Spring Boot 3 전환 (ACX-E2E-1785394564819)
Some checks are pending
Verify / source-contract (push) Waiting to run

This commit is contained in:
forge-bot 2026-07-30 10:38:09 +00:00
parent d671e38cba
commit b3f0574fc1

View file

@ -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<AcquisitionApprovalResponse> getAcquisitionsByMerchant(String merchantId) {
return repository.findByMerchantIdAndCancelYn(merchantId, "N")
.stream().map(this::mapToResponse).collect(Collectors.toList());
}
@Transactional(readOnly = true)
public List<AcquisitionApprovalResponse> 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<Map<String, Object>> 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<String, Object> 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<Map<String, Object>> handleValidationException(MethodArgumentNotValidException ex) {
Map<String, String> fieldErrors = new HashMap<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
fieldErrors.put(error.getField(), error.getDefaultMessage());
}
Map<String, Object> 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<Map<String, Object>> handleGenericException(Exception ex) {
log.error("예상치 못한 예외 발생", ex);
Map<String, Object> 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);
}
}
}