feat(core): RecruitLedger/MaintainLedger sumByContract 메서드 추가 (P1 보강)
P1-1-c RegulatoryLimitChecker의 누적 수수료 합산용 TODO 해결. 추가 메서드: - RecruitLedgerMapper.sumByContract(contractId) - RecruitLedgerMapper.sumFirstYearByContract(contractId) — commission_year=1 한정 - MaintainLedgerMapper.sumByContract(contractId) XML: COALESCE(SUM(agent_amount), 0) 패턴 (NULL 안전). 참고: V5 DDL 실제 컬럼명은 agent_amount (commission_amount 아님). api-developer가 RegulatoryLimitChecker에서 이 메서드를 호출하면 TODO 해소.
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
package com.ga.api.controller.installment;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Data
|
||||
public class GeneratePlanReq {
|
||||
|
||||
@NotNull
|
||||
private Long contractId;
|
||||
|
||||
@NotNull
|
||||
@Positive
|
||||
private BigDecimal totalCommission;
|
||||
|
||||
@NotNull
|
||||
private LocalDate contractDate;
|
||||
|
||||
private String productCategory;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.ga.api.controller.installment;
|
||||
|
||||
import com.ga.api.service.installment.InstallmentService;
|
||||
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.installment.InstallmentPlanResp;
|
||||
import com.ga.core.vo.installment.InstallmentPlanSearchParam;
|
||||
import com.ga.core.vo.installment.InstallmentPlanVO;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "분급 계획 관리")
|
||||
@RestController
|
||||
@RequestMapping("/api/installment-plans")
|
||||
@RequiredArgsConstructor
|
||||
public class InstallmentController {
|
||||
|
||||
private final InstallmentService installmentService;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "INSTALLMENT", perm = "READ")
|
||||
public ApiResponse<PageResponse<InstallmentPlanResp>> list(InstallmentPlanSearchParam param) {
|
||||
return ApiResponse.ok(installmentService.list(param));
|
||||
}
|
||||
|
||||
@GetMapping("/{planId}")
|
||||
@RequirePermission(menu = "INSTALLMENT", perm = "READ")
|
||||
public ApiResponse<InstallmentPlanResp> detail(@PathVariable Long planId) {
|
||||
return ApiResponse.ok(installmentService.detail(planId));
|
||||
}
|
||||
|
||||
@PostMapping("/generate")
|
||||
@RequirePermission(menu = "INSTALLMENT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "INSTALLMENT", table = "installment_plan")
|
||||
public ApiResponse<Void> generate(@Valid @RequestBody GeneratePlanReq req) {
|
||||
installmentService.createPlan(
|
||||
req.getContractId(),
|
||||
req.getTotalCommission(),
|
||||
req.getContractDate(),
|
||||
req.getProductCategory()
|
||||
);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PutMapping("/{planId}/pay")
|
||||
@RequirePermission(menu = "INSTALLMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "INSTALLMENT", table = "installment_plan")
|
||||
public ApiResponse<Void> pay(@PathVariable Long planId,
|
||||
@Valid @RequestBody PayPlanReq req) {
|
||||
installmentService.payPlan(planId, req.getPaymentId(), req.getPaidAmount());
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PutMapping("/{planId}/defer")
|
||||
@RequirePermission(menu = "INSTALLMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "INSTALLMENT", table = "installment_plan")
|
||||
public ApiResponse<Void> defer(@PathVariable Long planId) {
|
||||
installmentService.deferPlan(planId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@PutMapping("/{planId}/cancel")
|
||||
@RequirePermission(menu = "INSTALLMENT", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "INSTALLMENT", table = "installment_plan")
|
||||
public ApiResponse<Void> cancel(@PathVariable Long planId) {
|
||||
installmentService.cancelPlan(planId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@GetMapping("/scheduled")
|
||||
@RequirePermission(menu = "INSTALLMENT", perm = "READ")
|
||||
public ApiResponse<List<InstallmentPlanVO>> scheduled(@RequestParam String settleMonth) {
|
||||
return ApiResponse.ok(installmentService.selectScheduledForMonth(settleMonth));
|
||||
}
|
||||
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package com.ga.api.controller.installment;
|
||||
|
||||
import com.ga.api.service.installment.InstallmentService;
|
||||
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.installment.InstallmentRatioResp;
|
||||
import com.ga.core.vo.installment.InstallmentRatioSaveReq;
|
||||
import com.ga.core.vo.installment.InstallmentRatioSearchParam;
|
||||
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/installment-ratios")
|
||||
@RequiredArgsConstructor
|
||||
public class InstallmentRatioController {
|
||||
|
||||
private final InstallmentService installmentService;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "INSTALLMENT", perm = "READ")
|
||||
public ApiResponse<PageResponse<InstallmentRatioResp>> list(InstallmentRatioSearchParam param) {
|
||||
return ApiResponse.ok(installmentService.listRatios(param));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "INSTALLMENT", perm = "CREATE")
|
||||
@DataChangeLog(menu = "INSTALLMENT", table = "installment_ratio")
|
||||
public ApiResponse<Void> create(@Valid @RequestBody InstallmentRatioSaveReq req) {
|
||||
installmentService.createRatio(req);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.ga.api.controller.installment;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class PayPlanReq {
|
||||
|
||||
@NotNull
|
||||
private Long paymentId;
|
||||
|
||||
@NotNull
|
||||
@Positive
|
||||
private BigDecimal paidAmount;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.ga.api.service.installment;
|
||||
|
||||
import com.ga.core.mapper.installment.InstallmentRatioMapper;
|
||||
import com.ga.core.vo.installment.InstallmentPlanVO;
|
||||
import com.ga.core.vo.installment.InstallmentRatioVO;
|
||||
import com.ga.core.vo.installment.InstallmentStatus;
|
||||
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.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class InstallmentPlanGenerator {
|
||||
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyyMM");
|
||||
|
||||
private final InstallmentRatioMapper ratioMapper;
|
||||
|
||||
/**
|
||||
* 계약 체결일 기준 1~7차년 분급 계획 생성.
|
||||
* ratioPercent 합이 100%가 안 되는 경우 각 연차별 비율만큼 지급.
|
||||
*/
|
||||
public List<InstallmentPlanVO> generate(Long contractId, BigDecimal totalCommission,
|
||||
LocalDate contractDate, String productCategory) {
|
||||
String baseDate = contractDate.format(DATE_FMT);
|
||||
List<InstallmentRatioVO> ratios = ratioMapper.selectAllActiveRatios(productCategory, baseDate);
|
||||
|
||||
List<InstallmentPlanVO> plans = new ArrayList<>();
|
||||
for (InstallmentRatioVO ratio : ratios) {
|
||||
InstallmentPlanVO plan = new InstallmentPlanVO();
|
||||
plan.setContractId(contractId);
|
||||
plan.setPlanYear(ratio.getPlanYear());
|
||||
plan.setPlanMonth(1); // 단순화: 연 1회 지급 (plan_month=1)
|
||||
plan.setSettleMonth(contractDate.plusYears(ratio.getPlanYear()).format(MONTH_FMT));
|
||||
plan.setExpectedAmount(
|
||||
totalCommission.multiply(ratio.getRatioPercent())
|
||||
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP)
|
||||
);
|
||||
plan.setStatus(InstallmentStatus.SCHEDULED.code());
|
||||
plans.add(plan);
|
||||
}
|
||||
return plans;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.ga.api.service.installment;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.core.mapper.installment.InstallmentPlanMapper;
|
||||
import com.ga.core.mapper.installment.InstallmentRatioMapper;
|
||||
import com.ga.core.vo.installment.InstallmentPlanResp;
|
||||
import com.ga.core.vo.installment.InstallmentPlanSearchParam;
|
||||
import com.ga.core.vo.installment.InstallmentPlanVO;
|
||||
import com.ga.core.vo.installment.InstallmentRatioResp;
|
||||
import com.ga.core.vo.installment.InstallmentRatioSaveReq;
|
||||
import com.ga.core.vo.installment.InstallmentRatioSearchParam;
|
||||
import com.ga.core.vo.installment.InstallmentRatioVO;
|
||||
import com.ga.core.vo.installment.InstallmentStatus;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InstallmentService {
|
||||
|
||||
private final InstallmentPlanMapper planMapper;
|
||||
private final InstallmentRatioMapper ratioMapper;
|
||||
private final InstallmentPlanGenerator generator;
|
||||
|
||||
// ---- Plan 조회 ----
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<InstallmentPlanResp> list(InstallmentPlanSearchParam param) {
|
||||
param.startPage();
|
||||
return PageResponse.of(planMapper.selectList(param));
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public InstallmentPlanResp detail(Long planId) {
|
||||
InstallmentPlanResp resp = planMapper.selectDetailById(planId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<InstallmentPlanVO> selectScheduledForMonth(String settleMonth) {
|
||||
return planMapper.selectScheduledByMonth(settleMonth);
|
||||
}
|
||||
|
||||
// ---- Ratio 조회 ----
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<InstallmentRatioResp> listRatios(InstallmentRatioSearchParam param) {
|
||||
param.startPage();
|
||||
return PageResponse.of(ratioMapper.selectList(param));
|
||||
}
|
||||
|
||||
// ---- Plan 생성 ----
|
||||
|
||||
@Transactional
|
||||
public void createPlan(Long contractId, BigDecimal totalCommission,
|
||||
LocalDate contractDate, String productCategory) {
|
||||
List<InstallmentPlanVO> plans = generator.generate(contractId, totalCommission, contractDate, productCategory);
|
||||
if (!plans.isEmpty()) {
|
||||
planMapper.insertBatch(plans);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Plan 상태 전이 ----
|
||||
|
||||
@Transactional
|
||||
public void payPlan(Long planId, Long paymentId, BigDecimal paidAmount) {
|
||||
InstallmentPlanVO plan = requirePlan(planId);
|
||||
if (!InstallmentStatus.SCHEDULED.code().equals(plan.getStatus())
|
||||
&& !InstallmentStatus.DEFERRED.code().equals(plan.getStatus())) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER, "지급 처리 가능한 상태가 아닙니다");
|
||||
}
|
||||
plan.setStatus(InstallmentStatus.PAID.code());
|
||||
plan.setPaidAmount(paidAmount);
|
||||
plan.setPaidAt(LocalDateTime.now());
|
||||
plan.setPaymentId(paymentId);
|
||||
planMapper.update(plan);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deferPlan(Long planId) {
|
||||
InstallmentPlanVO plan = requirePlan(planId);
|
||||
if (!InstallmentStatus.SCHEDULED.code().equals(plan.getStatus())) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER, "SCHEDULED 상태만 유예 가능합니다");
|
||||
}
|
||||
planMapper.updateStatus(planId, InstallmentStatus.DEFERRED.code());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void cancelPlan(Long planId) {
|
||||
InstallmentPlanVO plan = requirePlan(planId);
|
||||
if (InstallmentStatus.PAID.code().equals(plan.getStatus())) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER, "지급 완료된 분급은 취소할 수 없습니다");
|
||||
}
|
||||
planMapper.updateStatus(planId, InstallmentStatus.CANCELLED.code());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void payScheduledForMonth(String settleMonth) {
|
||||
List<InstallmentPlanVO> scheduled = planMapper.selectScheduledByMonth(settleMonth);
|
||||
for (InstallmentPlanVO plan : scheduled) {
|
||||
plan.setStatus(InstallmentStatus.PAID.code());
|
||||
plan.setPaidAmount(plan.getExpectedAmount());
|
||||
plan.setPaidAt(LocalDateTime.now());
|
||||
planMapper.update(plan);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Ratio 생성 ----
|
||||
|
||||
@Transactional
|
||||
public void createRatio(InstallmentRatioSaveReq req) {
|
||||
InstallmentRatioVO vo = new InstallmentRatioVO();
|
||||
vo.setProductCategory(req.getProductCategory());
|
||||
vo.setPlanYear(req.getPlanYear());
|
||||
vo.setRatioPercent(req.getRatioPercent());
|
||||
vo.setEffectiveFrom(req.getEffectiveFrom());
|
||||
vo.setEffectiveTo(req.getEffectiveTo());
|
||||
vo.setIsActive(true);
|
||||
ratioMapper.insert(vo);
|
||||
}
|
||||
|
||||
private InstallmentPlanVO requirePlan(Long planId) {
|
||||
InstallmentPlanVO plan = planMapper.selectById(planId);
|
||||
if (plan == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,7 @@ public interface MaintainLedgerMapper {
|
||||
BigDecimal sumByMonth(@Param("settleMonth") String settleMonth);
|
||||
BigDecimal sumByAgentMonth(@Param("agentId") Long agentId,
|
||||
@Param("settleMonth") String settleMonth);
|
||||
|
||||
/** 계약별 유지 원장 수수료 합산 (1200%룰 누적 검증용) */
|
||||
BigDecimal sumByContract(@Param("contractId") Long contractId);
|
||||
}
|
||||
|
||||
@@ -30,4 +30,10 @@ public interface RecruitLedgerMapper {
|
||||
/** 정산월 + 설계사 합계 */
|
||||
BigDecimal sumByAgentMonth(@Param("agentId") Long agentId,
|
||||
@Param("settleMonth") String settleMonth);
|
||||
|
||||
/** 계약별 신계약 원장 수수료 합산 (1200%룰 누적 검증용) */
|
||||
BigDecimal sumByContract(@Param("contractId") Long contractId);
|
||||
|
||||
/** 계약별 1차년도 수수료 합산 (1차년도 한도 검증용) */
|
||||
BigDecimal sumFirstYearByContract(@Param("contractId") Long contractId);
|
||||
}
|
||||
|
||||
@@ -112,4 +112,9 @@
|
||||
WHERE agent_id = #{agentId} AND settle_month = #{settleMonth}
|
||||
</select>
|
||||
|
||||
<select id="sumByContract" resultType="java.math.BigDecimal">
|
||||
SELECT COALESCE(SUM(agent_amount), 0) FROM maintain_ledger
|
||||
WHERE contract_id = #{contractId}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -129,4 +129,14 @@
|
||||
WHERE agent_id = #{agentId} AND settle_month = #{settleMonth}
|
||||
</select>
|
||||
|
||||
<select id="sumByContract" resultType="java.math.BigDecimal">
|
||||
SELECT COALESCE(SUM(agent_amount), 0) FROM recruit_ledger
|
||||
WHERE contract_id = #{contractId}
|
||||
</select>
|
||||
|
||||
<select id="sumFirstYearByContract" resultType="java.math.BigDecimal">
|
||||
SELECT COALESCE(SUM(agent_amount), 0) FROM recruit_ledger
|
||||
WHERE contract_id = #{contractId} AND commission_year = 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user