fix: 오버라이드 insurance_type_cond 미적용 과지급 수정 (PL 우선순위 진행)

- fix(HIGH): CalcOverrideStep base 가 override_rule.insurance_type_cond 를 무시하고
  하위 설계사의 전 보험종류 실적 합계에 율을 곱해 과지급. 룰에 보험종류 조건이
  지정되면(LIFE 등) 해당 종류 실적만 base 로 집계하도록 수정.
  (현 시드는 insurance_type_cond NULL 이라 잠복상태였으나 종류한정 룰 도입 시 과지급)
- add: RecruitLedgerMapper/MaintainLedgerMapper.sumByAgentMonthAndType + XML
- test: CalcOverrideStepTest — LIFE 한정 룰 base 필터 검증
- 전체 build+test GREEN: 153건 0실패

잔여 백로그(정책결정 필요 — 자동수정 보류): 1200%룰 초과분 clip/이연(지급정책),
계약승계 from검증(다단계 체인+contract.agent_id 갱신정책), 정착지원금 환수공식(입력
스키마), 회계 원천세 예수금 분개(회계정책), net음수 클램프(이월저장), 세금 3중경로 통일.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-06-03 10:38:09 +09:00
parent eac0b813d6
commit 60c4cbae78
6 changed files with 137 additions and 3 deletions
@@ -80,7 +80,8 @@ public class CalcOverrideStep implements Tasklet {
lower.getOrgId(), rule.getToGrade(), lower.getAgentId(), upperCache);
if (upperAgentId == null) continue;
BigDecimal base = sumLowerCommission(lower.getAgentId(), ctx.getSettleMonth());
BigDecimal base = sumLowerCommission(
lower.getAgentId(), ctx.getSettleMonth(), rule.getInsuranceTypeCond());
if (MoneyUtil.isZero(base)) continue;
BigDecimal amount = MoneyUtil.pct(base, rule.getOverridePct());
@@ -105,8 +106,18 @@ public class CalcOverrideStep implements Tasklet {
return RepeatStatus.FINISHED;
}
/** 하위 설계사의 정산월 모집 + 유지 합계 */
private BigDecimal sumLowerCommission(Long agentId, String settleMonth) {
/**
* 하위 설계사의 정산월 모집 + 유지 합계.
* 룰에 insurance_type_cond 가 지정되면 해당 보험종류 실적만 base 로 집계.
* (지정 없음/ALL 이면 전 보험종류 합계 — 보험종류 한정 룰이 전체 실적에 곱해져 과지급되는 것을 방지)
*/
private BigDecimal sumLowerCommission(Long agentId, String settleMonth, String insuranceTypeCond) {
if (insuranceTypeCond != null && !insuranceTypeCond.isBlank()
&& !"ALL".equalsIgnoreCase(insuranceTypeCond)) {
BigDecimal r = recruitMapper.sumByAgentMonthAndType(agentId, settleMonth, insuranceTypeCond);
BigDecimal m = maintainMapper.sumByAgentMonthAndType(agentId, settleMonth, insuranceTypeCond);
return MoneyUtil.add(r, m);
}
BigDecimal r = recruitMapper.sumByAgentMonth(agentId, settleMonth);
BigDecimal m = maintainMapper.sumByAgentMonth(agentId, settleMonth);
return MoneyUtil.add(r, m);
@@ -0,0 +1,101 @@
package com.ga.batch.settlement.step;
import com.ga.batch.settlement.SettlementContext;
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
import com.ga.core.mapper.org.AgentMapper;
import com.ga.core.mapper.org.OrganizationMapper;
import com.ga.core.mapper.rule.RuleMapper;
import com.ga.core.mapper.settle.OverrideSettleMapper;
import com.ga.core.vo.org.AgentResp;
import com.ga.core.vo.org.AgentSearchParam;
import com.ga.core.vo.rule.OverrideRuleVO;
import com.ga.core.vo.settle.OverrideSettleVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import java.math.BigDecimal;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 오버라이드 보험종류 조건(insurance_type_cond) 적용 검증.
* 룰에 insurance_type_cond='LIFE' 가 있으면 base 가 LIFE 실적만으로 집계되어야 한다
* (전 보험종류 합계에 율을 곱해 과지급되던 결함 수정).
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class CalcOverrideStepTest {
@Mock SettlementContext ctx;
@Mock RuleMapper ruleMapper;
@Mock AgentMapper agentMapper;
@Mock OrganizationMapper organizationMapper;
@Mock OverrideSettleMapper overrideMapper;
@Mock RecruitLedgerMapper recruitMapper;
@Mock MaintainLedgerMapper maintainMapper;
CalcOverrideStep step;
static final String MONTH = "202506";
static final Long LOWER = 10L, UPPER = 99L, ORG = 100L;
@BeforeEach
void setUp() {
step = new CalcOverrideStep(ctx, ruleMapper, agentMapper,
organizationMapper, overrideMapper, recruitMapper, maintainMapper);
when(ctx.getSettleMonth()).thenReturn(MONTH);
// 하위 설계사(등급5) 1명 / 상위(등급1) 1명
AgentResp lower = new AgentResp();
lower.setAgentId(LOWER); lower.setOrgId(ORG); lower.setGradeId(5L);
AgentResp upper = new AgentResp();
upper.setAgentId(UPPER); upper.setOrgId(ORG); upper.setGradeId(1L);
when(agentMapper.selectList(any(AgentSearchParam.class))).thenAnswer(inv -> {
AgentSearchParam p = inv.getArgument(0);
return p.getOrgId() != null ? List.of(upper) : List.of(lower); // 상위탐색 vs 전체ACTIVE
});
when(organizationMapper.selectAncestorIds(ORG)).thenReturn(List.of(ORG));
}
@Test
@DisplayName("insurance_type_cond='LIFE' 룰: base 는 LIFE 실적만 집계, 전체합 미사용")
void overrideBase_filteredByInsuranceType() {
OverrideRuleVO rule = new OverrideRuleVO();
rule.setFromGrade(5L);
rule.setToGrade(1L);
rule.setOverridePct(new BigDecimal("4")); // 4%
rule.setInsuranceTypeCond("LIFE");
when(ruleMapper.selectOverrideRules()).thenReturn(new java.util.ArrayList<>(List.of(rule)));
// LIFE 실적만 1,000,000 (전체합은 호출되면 안 됨)
when(recruitMapper.sumByAgentMonthAndType(LOWER, MONTH, "LIFE")).thenReturn(new BigDecimal("1000000"));
when(maintainMapper.sumByAgentMonthAndType(LOWER, MONTH, "LIFE")).thenReturn(BigDecimal.ZERO);
ArgumentCaptor<List<OverrideSettleVO>> captor = ArgumentCaptor.forClass(List.class);
when(overrideMapper.insertBatch(captor.capture())).thenReturn(1);
step.execute(null, null);
List<OverrideSettleVO> saved = captor.getValue();
assertThat(saved).hasSize(1);
assertThat(saved.get(0).getBaseAmount()).isEqualByComparingTo("1000000"); // LIFE만
assertThat(saved.get(0).getOverrideAmount()).isEqualByComparingTo("40000.00"); // ×4%
// 보험종류 한정 룰이면 전체합 집계는 호출되지 않아야 함
verify(recruitMapper, never()).sumByAgentMonth(eq(LOWER), eq(MONTH));
verify(maintainMapper, never()).sumByAgentMonth(eq(LOWER), eq(MONTH));
}
}
@@ -27,6 +27,11 @@ public interface MaintainLedgerMapper {
BigDecimal sumByAgentMonth(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth);
/** 정산월 + 설계사 + 보험종류 합계 (오버라이드 insurance_type_cond 적용용) */
BigDecimal sumByAgentMonthAndType(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth,
@Param("insuranceType") String insuranceType);
/** 계약별 유지 원장 수수료 합산 (1200%룰 누적 검증용) */
BigDecimal sumByContract(@Param("contractId") Long contractId);
}
@@ -31,6 +31,11 @@ public interface RecruitLedgerMapper {
BigDecimal sumByAgentMonth(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth);
/** 정산월 + 설계사 + 보험종류 합계 (오버라이드 insurance_type_cond 적용용) */
BigDecimal sumByAgentMonthAndType(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth,
@Param("insuranceType") String insuranceType);
/** 계약별 신계약 원장 수수료 합산 (1200%룰 누적 검증용) */
BigDecimal sumByContract(@Param("contractId") Long contractId);
@@ -112,6 +112,12 @@
WHERE agent_id = #{agentId} AND settle_month = #{settleMonth}
</select>
<select id="sumByAgentMonthAndType" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(agent_amount), 0) FROM maintain_ledger
WHERE agent_id = #{agentId} AND settle_month = #{settleMonth}
AND insurance_type = #{insuranceType}
</select>
<select id="sumByContract" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(agent_amount), 0) FROM maintain_ledger
WHERE contract_id = #{contractId}
@@ -129,6 +129,12 @@
WHERE agent_id = #{agentId} AND settle_month = #{settleMonth}
</select>
<select id="sumByAgentMonthAndType" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(agent_amount), 0) FROM recruit_ledger
WHERE agent_id = #{agentId} AND settle_month = #{settleMonth}
AND insurance_type = #{insuranceType}
</select>
<select id="sumByContract" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(agent_amount), 0) FROM recruit_ledger
WHERE contract_id = #{contractId}