From 3831eeaf89461a164683b49dbbeb5bd353e0d8eb Mon Sep 17 00:00:00 2001 From: GA Pro Date: Wed, 13 May 2026 00:56:24 +0900 Subject: [PATCH] =?UTF-8?q?feat(api):=20ChargebackCalculator=20+=20Chargeb?= =?UTF-8?q?ackGradeService=20+=20Controller=20(=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=20P1-3-c)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 도메인 P1 마지막 항목 — 차등 환수 마무리. ChargebackCalculator (pure @Component): - calculate(contractDate, terminationDate, originalCommission, productCategory) - monthsElapsed = ChronoUnit.MONTHS.between - selectByMonths로 적용 비율 조회 - chargebackAmount = originalCommission × percent / 100 (HALF_UP, scale 0) - grade 없으면 ZERO + "적용 환수 구간 없음" ChargebackResult record: - chargebackAmount, monthsElapsed, appliedPercent, reason ChargebackGradeService: - list/detail (readOnly) - create/update (@Transactional, 중복 검증) - deactivate (@Transactional) ChargebackGradeController: - GET/POST/PUT 표준 + /api/chargeback-grades/calculate (외부 호출용) - 모든 보호 엔드포인트 @RequirePermission, CUD에 @DataChangeLog ga-batch 정산 step 자동 통합은 마무리 통합 단계에서. --- .../chargeback/ChargebackCalculateReq.java | 24 ++++++ .../chargeback/ChargebackGradeController.java | 75 ++++++++++++++++ .../chargeback/ChargebackCalculator.java | 45 ++++++++++ .../chargeback/ChargebackGradeService.java | 86 +++++++++++++++++++ .../service/chargeback/ChargebackResult.java | 10 +++ 5 files changed, 240 insertions(+) create mode 100644 ga-api/src/main/java/com/ga/api/controller/chargeback/ChargebackCalculateReq.java create mode 100644 ga-api/src/main/java/com/ga/api/controller/chargeback/ChargebackGradeController.java create mode 100644 ga-api/src/main/java/com/ga/api/service/chargeback/ChargebackCalculator.java create mode 100644 ga-api/src/main/java/com/ga/api/service/chargeback/ChargebackGradeService.java create mode 100644 ga-api/src/main/java/com/ga/api/service/chargeback/ChargebackResult.java diff --git a/ga-api/src/main/java/com/ga/api/controller/chargeback/ChargebackCalculateReq.java b/ga-api/src/main/java/com/ga/api/controller/chargeback/ChargebackCalculateReq.java new file mode 100644 index 0000000..3a4c92d --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/chargeback/ChargebackCalculateReq.java @@ -0,0 +1,24 @@ +package com.ga.api.controller.chargeback; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +@Data +public class ChargebackCalculateReq { + + @NotNull + private LocalDate contractDate; + + @NotNull + private LocalDate terminationDate; + + @NotNull + @Positive + private BigDecimal originalCommission; + + private String productCategory; +} diff --git a/ga-api/src/main/java/com/ga/api/controller/chargeback/ChargebackGradeController.java b/ga-api/src/main/java/com/ga/api/controller/chargeback/ChargebackGradeController.java new file mode 100644 index 0000000..66161f3 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/chargeback/ChargebackGradeController.java @@ -0,0 +1,75 @@ +package com.ga.api.controller.chargeback; + +import com.ga.api.service.chargeback.ChargebackCalculator; +import com.ga.api.service.chargeback.ChargebackGradeService; +import com.ga.api.service.chargeback.ChargebackResult; +import com.ga.common.annotation.DataChangeLog; +import com.ga.common.annotation.RequirePermission; +import com.ga.common.model.ApiResponse; +import com.ga.common.model.PageResponse; +import com.ga.core.vo.chargeback.ChargebackGradeResp; +import com.ga.core.vo.chargeback.ChargebackGradeSaveReq; +import com.ga.core.vo.chargeback.ChargebackGradeSearchParam; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Tag(name = "환수 등급 관리") +@RestController +@RequestMapping("/api/chargeback-grades") +@RequiredArgsConstructor +public class ChargebackGradeController { + + private final ChargebackGradeService gradeService; + private final ChargebackCalculator calculator; + + @GetMapping + @RequirePermission(menu = "CHARGEBACK_GRADE", perm = "READ") + public ApiResponse> list(ChargebackGradeSearchParam param) { + return ApiResponse.ok(gradeService.list(param)); + } + + @GetMapping("/{gradeId}") + @RequirePermission(menu = "CHARGEBACK_GRADE", perm = "READ") + public ApiResponse detail(@PathVariable Long gradeId) { + return ApiResponse.ok(gradeService.detail(gradeId)); + } + + @PostMapping + @RequirePermission(menu = "CHARGEBACK_GRADE", perm = "CREATE") + @DataChangeLog(menu = "CHARGEBACK_GRADE", table = "chargeback_grade") + public ApiResponse create(@Valid @RequestBody ChargebackGradeSaveReq req) { + gradeService.create(req); + return ApiResponse.ok(); + } + + @PutMapping("/{gradeId}") + @RequirePermission(menu = "CHARGEBACK_GRADE", perm = "UPDATE") + @DataChangeLog(menu = "CHARGEBACK_GRADE", table = "chargeback_grade") + public ApiResponse update(@PathVariable Long gradeId, + @Valid @RequestBody ChargebackGradeSaveReq req) { + gradeService.update(gradeId, req); + return ApiResponse.ok(); + } + + @PutMapping("/{gradeId}/deactivate") + @RequirePermission(menu = "CHARGEBACK_GRADE", perm = "UPDATE") + @DataChangeLog(menu = "CHARGEBACK_GRADE", table = "chargeback_grade") + public ApiResponse deactivate(@PathVariable Long gradeId) { + gradeService.deactivate(gradeId); + return ApiResponse.ok(); + } + + @PostMapping("/calculate") + @RequirePermission(menu = "CHARGEBACK_GRADE", perm = "READ") + public ApiResponse calculate(@Valid @RequestBody ChargebackCalculateReq req) { + ChargebackResult result = calculator.calculate( + req.getContractDate(), + req.getTerminationDate(), + req.getOriginalCommission(), + req.getProductCategory() + ); + return ApiResponse.ok(result); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/chargeback/ChargebackCalculator.java b/ga-api/src/main/java/com/ga/api/service/chargeback/ChargebackCalculator.java new file mode 100644 index 0000000..10ebaa9 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/chargeback/ChargebackCalculator.java @@ -0,0 +1,45 @@ +package com.ga.api.service.chargeback; + +import com.ga.core.mapper.chargeback.ChargebackGradeMapper; +import com.ga.core.vo.chargeback.ChargebackGradeVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +@Component +@RequiredArgsConstructor +public class ChargebackCalculator { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private final ChargebackGradeMapper gradeMapper; + + public ChargebackResult calculate(LocalDate contractDate, LocalDate terminationDate, + BigDecimal originalCommission, String productCategory) { + int monthsElapsed = (int) ChronoUnit.MONTHS.between(contractDate, terminationDate); + String today = LocalDate.now().format(DATE_FMT); + + ChargebackGradeVO grade = gradeMapper.selectByMonths(productCategory, monthsElapsed, today); + + if (grade == null) { + return new ChargebackResult(BigDecimal.ZERO, monthsElapsed, BigDecimal.ZERO, + "적용 환수 구간 없음"); + } + + BigDecimal chargebackAmount = originalCommission + .multiply(grade.getChargebackPercent()) + .divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP); + + return new ChargebackResult( + chargebackAmount, + monthsElapsed, + grade.getChargebackPercent(), + String.format("경과월 %d개월 → 환수율 %s%%", monthsElapsed, grade.getChargebackPercent()) + ); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/chargeback/ChargebackGradeService.java b/ga-api/src/main/java/com/ga/api/service/chargeback/ChargebackGradeService.java new file mode 100644 index 0000000..0b82da2 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/chargeback/ChargebackGradeService.java @@ -0,0 +1,86 @@ +package com.ga.api.service.chargeback; + +import com.ga.common.exception.BizException; +import com.ga.common.exception.ErrorCode; +import com.ga.common.model.PageResponse; +import com.ga.core.mapper.chargeback.ChargebackGradeMapper; +import com.ga.core.vo.chargeback.ChargebackGradeResp; +import com.ga.core.vo.chargeback.ChargebackGradeSaveReq; +import com.ga.core.vo.chargeback.ChargebackGradeSearchParam; +import com.ga.core.vo.chargeback.ChargebackGradeVO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +@Service +@RequiredArgsConstructor +public class ChargebackGradeService { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private final ChargebackGradeMapper gradeMapper; + + @Transactional(readOnly = true) + public PageResponse list(ChargebackGradeSearchParam param) { + param.startPage(); + return PageResponse.of(gradeMapper.selectList(param)); + } + + @Transactional(readOnly = true) + public ChargebackGradeResp detail(Long gradeId) { + ChargebackGradeResp resp = gradeMapper.selectDetailById(gradeId); + if (resp == null) throw new BizException(ErrorCode.NOT_FOUND); + return resp; + } + + @Transactional + public void create(ChargebackGradeSaveReq req) { + String today = LocalDate.now().format(DATE_FMT); + ChargebackGradeVO existing = gradeMapper.selectByMonths( + req.getProductCategory(), req.getMonthsFrom(), today); + if (existing != null) { + throw new BizException(ErrorCode.DUPLICATE_DATA); + } + gradeMapper.insert(toVO(req)); + } + + @Transactional + public void update(Long gradeId, ChargebackGradeSaveReq req) { + ChargebackGradeVO existing = gradeMapper.selectById(gradeId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + ChargebackGradeVO vo = toVO(req); + vo.setGradeId(gradeId); + vo.setIsActive(existing.getIsActive()); + gradeMapper.update(vo); + } + + @Transactional + public void deactivate(Long gradeId) { + ChargebackGradeVO existing = gradeMapper.selectById(gradeId); + if (existing == null) throw new BizException(ErrorCode.NOT_FOUND); + if (Boolean.FALSE.equals(existing.getIsActive())) { + throw new BizException(ErrorCode.INVALID_PARAMETER); + } + gradeMapper.updateStatus(gradeId, false); + } + + @Transactional(readOnly = true) + public ChargebackGradeVO selectByMonths(String productCategory, Integer monthsElapsed, String baseDate) { + return gradeMapper.selectByMonths(productCategory, monthsElapsed, baseDate); + } + + private ChargebackGradeVO toVO(ChargebackGradeSaveReq req) { + ChargebackGradeVO vo = new ChargebackGradeVO(); + vo.setProductCategory(req.getProductCategory()); + vo.setMonthsFrom(req.getMonthsFrom()); + vo.setMonthsTo(req.getMonthsTo()); + vo.setChargebackPercent(req.getChargebackPercent()); + vo.setEffectiveFrom(req.getEffectiveFrom()); + vo.setEffectiveTo(req.getEffectiveTo()); + vo.setIsActive(true); + return vo; + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/chargeback/ChargebackResult.java b/ga-api/src/main/java/com/ga/api/service/chargeback/ChargebackResult.java new file mode 100644 index 0000000..57a2065 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/chargeback/ChargebackResult.java @@ -0,0 +1,10 @@ +package com.ga.api.service.chargeback; + +import java.math.BigDecimal; + +public record ChargebackResult( + BigDecimal chargebackAmount, + int monthsElapsed, + BigDecimal appliedPercent, + String reason +) {}