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:
+59
@@ -0,0 +1,59 @@
|
||||
package com.ga.api.controller.succession;
|
||||
|
||||
import com.ga.api.service.succession.ContractSuccessionService;
|
||||
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.succession.ContractSuccessionResp;
|
||||
import com.ga.core.vo.succession.ContractSuccessionSaveReq;
|
||||
import com.ga.core.vo.succession.ContractSuccessionSearchParam;
|
||||
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/contract-successions")
|
||||
@RequiredArgsConstructor
|
||||
public class ContractSuccessionController {
|
||||
|
||||
private final ContractSuccessionService service;
|
||||
|
||||
@GetMapping
|
||||
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "READ")
|
||||
public ApiResponse<PageResponse<ContractSuccessionResp>> list(
|
||||
@ModelAttribute ContractSuccessionSearchParam param) {
|
||||
return ApiResponse.ok(service.list(param));
|
||||
}
|
||||
|
||||
@GetMapping("/{successionId}")
|
||||
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "READ")
|
||||
public ApiResponse<ContractSuccessionResp> detail(@PathVariable Long successionId) {
|
||||
return ApiResponse.ok(service.detail(successionId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "CREATE")
|
||||
@DataChangeLog(menu = "CONTRACT_SUCCESSION", table = "contract_succession")
|
||||
public ApiResponse<Long> create(@Valid @RequestBody ContractSuccessionSaveReq req) {
|
||||
return ApiResponse.ok(service.create(req));
|
||||
}
|
||||
|
||||
@PostMapping("/{successionId}/cancel")
|
||||
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "UPDATE")
|
||||
@DataChangeLog(menu = "CONTRACT_SUCCESSION", table = "contract_succession")
|
||||
public ApiResponse<Void> cancel(@PathVariable Long successionId) {
|
||||
service.cancel(successionId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
|
||||
@DeleteMapping("/{successionId}")
|
||||
@RequirePermission(menu = "CONTRACT_SUCCESSION", perm = "DELETE")
|
||||
@DataChangeLog(menu = "CONTRACT_SUCCESSION", table = "contract_succession")
|
||||
public ApiResponse<Void> delete(@PathVariable Long successionId) {
|
||||
service.delete(successionId);
|
||||
return ApiResponse.ok();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.ga.api.service.succession;
|
||||
|
||||
import com.ga.common.exception.BizException;
|
||||
import com.ga.common.exception.ErrorCode;
|
||||
import com.ga.common.model.PageResponse;
|
||||
import com.ga.common.util.SecurityUtil;
|
||||
import com.ga.core.mapper.succession.ContractSuccessionMapper;
|
||||
import com.ga.core.vo.succession.ContractSuccessionResp;
|
||||
import com.ga.core.vo.succession.ContractSuccessionSaveReq;
|
||||
import com.ga.core.vo.succession.ContractSuccessionSearchParam;
|
||||
import com.ga.core.vo.succession.ContractSuccessionVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
public class ContractSuccessionService {
|
||||
|
||||
private final ContractSuccessionMapper mapper;
|
||||
|
||||
public PageResponse<ContractSuccessionResp> list(ContractSuccessionSearchParam param) {
|
||||
param.startPage();
|
||||
return PageResponse.of(mapper.selectList(param));
|
||||
}
|
||||
|
||||
public ContractSuccessionResp detail(Long successionId) {
|
||||
ContractSuccessionResp resp = mapper.selectById(successionId);
|
||||
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Long create(ContractSuccessionSaveReq req) {
|
||||
if (req.getFromAgentId().equals(req.getToAgentId())) {
|
||||
throw new BizException(ErrorCode.INVALID_PARAMETER, "전임과 후임 설계사가 같을 수 없습니다");
|
||||
}
|
||||
ContractSuccessionVO vo = new ContractSuccessionVO();
|
||||
vo.setContractId(req.getContractId());
|
||||
vo.setFromAgentId(req.getFromAgentId());
|
||||
vo.setToAgentId(req.getToAgentId());
|
||||
vo.setSuccessionDate(req.getSuccessionDate());
|
||||
vo.setReason(req.getReason());
|
||||
vo.setMemo(req.getMemo());
|
||||
vo.setCreatedBy(SecurityUtil.getCurrentUserId());
|
||||
mapper.insert(vo);
|
||||
return vo.getSuccessionId();
|
||||
}
|
||||
|
||||
/** 승계 취소 — status=CANCELED. 이후 정산월부터 유지수수료는 다시 직전 귀속 설계사에게. */
|
||||
@Transactional
|
||||
public void cancel(Long successionId) {
|
||||
ContractSuccessionResp existing = mapper.selectById(successionId);
|
||||
if (existing == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
mapper.cancel(successionId, SecurityUtil.getCurrentUserId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void delete(Long successionId) {
|
||||
ContractSuccessionResp existing = mapper.selectById(successionId);
|
||||
if (existing == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||
mapper.delete(successionId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user