feat: 계약 승계(contract succession) — 유지수수료 귀속 후임 전환 (V86)
설계사 해촉/이관 시 보험계약의 유지수수료 귀속을 승계월 기준 후임 설계사로 전환. 모집수수료는 원 모집설계사 귀속 유지(고아계약 승계 표준). - DB V86: contract_succession 테이블 + 인덱스 + 메뉴/권한(GRP_PRD) + 공통코드 2그룹 + 테스트데이터 1건 - core: ContractSuccession VO/Resp/SaveReq/SearchParam + Mapper(.java/.xml) selectEffectiveSuccessions(settleMonth): 계약별 최근 ACTIVE 승계 1건 (DISTINCT ON) - api: ContractSuccessionService/Controller (/api/contract-successions, CRUD+cancel) - batch: CalcMaintainStep 이 effective 승계 사전로드(N+1 회피) 후 effectiveAgentId 결정, CommissionCalculator.calcMaintainLedger 오버로드로 후임 등급 payout + ledger agent_id 후임 기록. AggregateStep 은 ledger agent_id 집계라 무변경. - frontend: api/succession.ts + ContractSuccessions.tsx + App.tsx 라우트 검증: 5모듈 compileJava SUCCESS / ga-frontend tsc 0 / Flyway v86 적용 / API CRUD+cancel+from==to 가드(E411) 라이브 PASS / resolver 쿼리 라이브 검증(계약1→후임 agent2). 배치 풀잡 e2e 는 정산월 원장 재생성(파괴적)이라 미실행. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,15 +26,25 @@ public class CommissionCalculator {
|
||||
private final AgentMapper agentMapper;
|
||||
|
||||
public LedgerVO calcRecruitLedger(ContractResp c, String settleMonth) {
|
||||
return buildLedger(c, settleMonth, 1);
|
||||
return buildLedger(c, settleMonth, 1, c.getAgentId());
|
||||
}
|
||||
|
||||
public LedgerVO calcMaintainLedger(ContractResp c, String settleMonth, int commissionYear) {
|
||||
return buildLedger(c, settleMonth, commissionYear);
|
||||
return buildLedger(c, settleMonth, commissionYear, c.getAgentId());
|
||||
}
|
||||
|
||||
private LedgerVO buildLedger(ContractResp c, String settleMonth, int year) {
|
||||
AgentVO agent = agentMapper.selectById(c.getAgentId());
|
||||
/**
|
||||
* 계약 승계 반영 유지수수료 계산.
|
||||
* effectiveAgentId 가 후임 설계사이면 해당 설계사 등급의 payout 비율로 계산되고
|
||||
* 원장 agent_id 도 후임으로 기록된다. null 이면 원 모집설계사로 동작.
|
||||
*/
|
||||
public LedgerVO calcMaintainLedger(ContractResp c, String settleMonth, int commissionYear, Long effectiveAgentId) {
|
||||
return buildLedger(c, settleMonth, commissionYear,
|
||||
effectiveAgentId != null ? effectiveAgentId : c.getAgentId());
|
||||
}
|
||||
|
||||
private LedgerVO buildLedger(ContractResp c, String settleMonth, int year, Long agentId) {
|
||||
AgentVO agent = agentMapper.selectById(agentId);
|
||||
if (agent == null) return null;
|
||||
LocalDate baseDate = c.getContractDate() != null ? c.getContractDate() : LocalDate.now();
|
||||
|
||||
@@ -52,7 +62,7 @@ public class CommissionCalculator {
|
||||
|
||||
LedgerVO led = new LedgerVO();
|
||||
led.setContractId(c.getContractId());
|
||||
led.setAgentId(c.getAgentId());
|
||||
led.setAgentId(agentId);
|
||||
led.setPolicyNo(c.getPolicyNo());
|
||||
led.setCompanyCode(c.getCompanyCode());
|
||||
led.setProductCode(c.getProductCode());
|
||||
|
||||
@@ -6,9 +6,11 @@ import com.ga.common.util.DateUtil;
|
||||
import com.ga.common.util.MoneyUtil;
|
||||
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
|
||||
import com.ga.core.mapper.product.ContractMapper;
|
||||
import com.ga.core.mapper.succession.ContractSuccessionMapper;
|
||||
import com.ga.core.vo.ledger.LedgerVO;
|
||||
import com.ga.core.vo.product.ContractResp;
|
||||
import com.ga.core.vo.product.ContractSearchParam;
|
||||
import com.ga.core.vo.succession.ContractSuccessionVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
@@ -20,7 +22,9 @@ import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Step 5 — calcMaintain: 유지수수료 계산 (2회차~)
|
||||
@@ -37,6 +41,7 @@ public class CalcMaintainStep implements Tasklet {
|
||||
private final SettlementContext ctx;
|
||||
private final ContractMapper contractMapper;
|
||||
private final MaintainLedgerMapper maintainMapper;
|
||||
private final ContractSuccessionMapper successionMapper;
|
||||
private final CommissionCalculator calculator;
|
||||
|
||||
private static final int BATCH_SIZE = 1000;
|
||||
@@ -52,6 +57,15 @@ public class CalcMaintainStep implements Tasklet {
|
||||
|
||||
maintainMapper.deleteBySettleMonth(ctx.getSettleMonth());
|
||||
|
||||
// 계약 승계 반영: 정산월 기준 유효 승계(계약→후임설계사) 사전 로드 (N+1 회피)
|
||||
Map<Long, Long> successorByContract = new HashMap<>();
|
||||
for (ContractSuccessionVO s : successionMapper.selectEffectiveSuccessions(ctx.getSettleMonth())) {
|
||||
successorByContract.put(s.getContractId(), s.getToAgentId());
|
||||
}
|
||||
if (!successorByContract.isEmpty()) {
|
||||
log.info("[Step5] active successions applied: {}", successorByContract.size());
|
||||
}
|
||||
|
||||
List<LedgerVO> batch = new ArrayList<>(BATCH_SIZE);
|
||||
int total = 0;
|
||||
|
||||
@@ -66,7 +80,9 @@ public class CalcMaintainStep implements Tasklet {
|
||||
// 납입주기 매칭 (월납이면 매월, 분기납이면 3개월마다 ...)
|
||||
if (!matchesPayCycle(c.getPayCycle(), monthsElapsed)) continue;
|
||||
|
||||
LedgerVO led = calculator.calcMaintainLedger(c, ctx.getSettleMonth(), year);
|
||||
// 승계된 계약이면 유지수수료 귀속을 후임 설계사로 (없으면 원 모집설계사)
|
||||
Long effectiveAgentId = successorByContract.getOrDefault(c.getContractId(), c.getAgentId());
|
||||
LedgerVO led = calculator.calcMaintainLedger(c, ctx.getSettleMonth(), year, effectiveAgentId);
|
||||
if (led == null || MoneyUtil.isZero(led.getAgentAmount())) continue;
|
||||
batch.add(led);
|
||||
if (batch.size() >= BATCH_SIZE) {
|
||||
|
||||
Reference in New Issue
Block a user