카드 매입 승인 경로 Spring Boot 3 전환 #7
1 changed files with 215 additions and 0 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue