feat: P11(#1) 시상/신인시상 환수 — award_ledger 환수 처리 (PL 자율진행)

incar 유지율시상환수/신인도입유치자시상환수 대응. award_ledger(V92)는 지급만 있고
환수가 없던 갭. 정착지원금/생보운영지원/지점장수당의 clawback_amount+net_amount 패턴으로
통일(신인시상=award_type NEW_RECRUIT로 기존재 → 별도 테이블 불필요).

- DB(V114): award_ledger에 clawback_amount/net_amount 컬럼 추가 + net=award 백필 +
  CHECK(clawback>=0) + status CHECK에 CLAWED_BACK 추가(pg_constraint 동적 탐색 DROP/재생성).
- core: AwardLedgerVO/Resp/SaveReq에 clawbackAmount/netAmount. AwardLedgerMapper
  aggregateByAgent SUM(award_amount)→SUM(net_amount), insert에 두 컬럼, applyClawback UPDATE.
- api: AwardService.createLedger가 net=award−clawback 계산(clawback≤award 검증). 신규
  applyClawback(ledgerId, {clawbackAmount,reason}) — 초과/취소상태 E411, status=CLAWED_BACK.
  PUT /api/awards/ledgers/{id}/clawback.
- frontend: Award 원장 그리드에 환수액/실지급 컬럼 + 합계 + 환수처리 모달(PermissionButton UPDATE).
- AggregateStep 무변경: aggregateByAgent가 net 반환 → 환수가 other_commission 자동 차감,
  net<0이면 P9 환수채권 이월에 자연 연계.

검증: build+test SUCCESSFUL, Flyway V114(schema v114). 라이브: award 100000 원장 →
clawback 30000 → net 70000·CLAWED_BACK, 초과환수 200000 → E411, aggregate net=70000 반영
확인. 테스트 데이터 정리. 스펙 docs/DOMAIN_GAP_P11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-06-05 00:59:48 +09:00
parent 284ecc4203
commit 934e45d295
12 changed files with 307 additions and 18 deletions
@@ -0,0 +1,20 @@
package com.ga.api.controller.commission;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* 시상 원장 환수 처리 요청 (PUT /api/awards/ledgers/{id}/clawback).
*/
@Data
public class AwardClawbackReq {
@NotNull
@DecimalMin("0")
private BigDecimal clawbackAmount;
private String reason;
}
@@ -79,4 +79,13 @@ public class AwardController {
public ApiResponse<Long> createLedger(@Valid @RequestBody AwardLedgerSaveReq req) {
return ApiResponse.ok(service.createLedger(req));
}
@PutMapping("/ledgers/{ledgerId}/clawback")
@RequirePermission(menu = "AWARD", perm = "UPDATE")
@DataChangeLog(menu = "AWARD", table = "award_ledger")
public ApiResponse<Void> applyClawback(@PathVariable Long ledgerId,
@Valid @RequestBody AwardClawbackReq req) {
service.applyClawback(ledgerId, req);
return ApiResponse.ok();
}
}
@@ -112,14 +112,38 @@ public class AwardService {
.setScale(2, RoundingMode.HALF_UP);
}
BigDecimal clawback = req.getClawbackAmount() != null ? req.getClawbackAmount() : BigDecimal.ZERO;
if (clawback.compareTo(awardAmount) > 0) {
throw new BizException(ErrorCode.INVALID_PARAMETER, "환수액이 시상액을 초과합니다");
}
BigDecimal netAmount = awardAmount.subtract(clawback).setScale(2, RoundingMode.HALF_UP);
AwardLedgerVO vo = new AwardLedgerVO();
vo.setAgentId(req.getAgentId());
vo.setSettleMonth(req.getSettleMonth());
vo.setAwardType(req.getAwardType());
vo.setBaseAmount(req.getBaseAmount());
vo.setAwardAmount(awardAmount);
vo.setClawbackAmount(clawback);
vo.setNetAmount(netAmount);
vo.setStatus("CALCULATED");
ledgerMapper.insert(vo);
return vo.getLedgerId();
}
@Transactional
public void applyClawback(Long ledgerId, com.ga.api.controller.commission.AwardClawbackReq req) {
AwardLedgerResp ledger = ledgerMapper.selectById(ledgerId);
if (ledger == null) throw new BizException(ErrorCode.NOT_FOUND);
if ("CANCELLED".equals(ledger.getStatus())) {
throw new BizException(ErrorCode.INVALID_PARAMETER, "취소된 원장에는 환수를 적용할 수 없습니다");
}
if (req.getClawbackAmount().compareTo(ledger.getAwardAmount()) > 0) {
throw new BizException(ErrorCode.INVALID_PARAMETER, "환수액이 시상액을 초과합니다");
}
BigDecimal netAmount = ledger.getAwardAmount()
.subtract(req.getClawbackAmount())
.setScale(2, RoundingMode.HALF_UP);
ledgerMapper.applyClawback(ledgerId, req.getClawbackAmount(), netAmount, "CLAWED_BACK");
}
}