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:
GA Pro
2026-05-29 21:28:00 +09:00
parent d8d64625a9
commit bd5ff4ec14
14 changed files with 660 additions and 6 deletions
+50
View File
@@ -0,0 +1,50 @@
import api, { PageResponse, unwrap } from './request';
export interface ContractSuccessionRow extends Record<string, unknown> {
successionId: number;
contractId: number;
policyNo: string;
fromAgentId: number;
fromAgentName: string;
toAgentId: number;
toAgentName: string;
successionDate: string;
reason: string; // TERMINATION / TRANSFER / REASSIGN
reasonName: string;
memo?: string;
status: string; // ACTIVE / CANCELED
statusName: string;
createdAt: string;
}
export interface ContractSuccessionSaveReq {
contractId: number;
fromAgentId: number;
toAgentId: number;
successionDate: string;
reason?: string;
memo?: string;
}
export interface SuccessionSearchParam {
contractId?: number;
fromAgentId?: number;
toAgentId?: number;
reason?: string;
status?: string;
pageNum?: number;
pageSize?: number;
}
export const successionApi = {
list: (p: SuccessionSearchParam) =>
unwrap<PageResponse<ContractSuccessionRow>>(
api.get('/api/contract-successions', { params: p }),
),
create: (body: ContractSuccessionSaveReq) =>
unwrap<number>(api.post('/api/contract-successions', body)),
cancel: (id: number) =>
unwrap<void>(api.post(`/api/contract-successions/${id}/cancel`)),
delete: (id: number) =>
unwrap<void>(api.delete(`/api/contract-successions/${id}`)),
};