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
@@ -31,5 +31,14 @@ public interface AwardLedgerMapper {
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
/**
* 환수 처리 — clawback_amount/net_amount/status/updated_at 갱신.
* status 는 통상 'CLAWED_BACK'. Service에서 clawback ≤ award_amount 검증 후 호출.
*/
int applyClawback(@Param("ledgerId") Long ledgerId,
@Param("clawbackAmount") BigDecimal clawbackAmount,
@Param("netAmount") BigDecimal netAmount,
@Param("status") String status);
int delete(@Param("ledgerId") Long ledgerId);
}
@@ -21,6 +21,10 @@ public class AwardLedgerResp {
private String awardTypeName;
private BigDecimal baseAmount;
private BigDecimal awardAmount;
/** 환수액 (V114) */
private BigDecimal clawbackAmount;
/** 실지급액 = award_amount - clawback_amount (V114) */
private BigDecimal netAmount;
private String status;
/** 공통코드 INCOME_STATUS */
private String statusName;
@@ -1,5 +1,6 @@
package com.ga.core.vo.commission;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
@@ -10,6 +11,7 @@ import java.math.BigDecimal;
/**
* award_ledger 등록 요청.
* award_amount 는 Service가 계산 후 VO에 set.
* net_amount 는 Service가 award_amount - clawbackAmount 계산 후 VO에 set.
*/
@Data
public class AwardLedgerSaveReq {
@@ -26,4 +28,8 @@ public class AwardLedgerSaveReq {
@NotNull
@Positive
private BigDecimal baseAmount;
/** 등록 시 환수액 (선택, 기본 0). clawback ≤ award_amount 검증은 Service에서 수행. */
@DecimalMin("0")
private BigDecimal clawbackAmount;
}
@@ -6,8 +6,9 @@ import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* award_ledger — 시상 원장 (V92)
* award_ledger — 시상 원장 (V92, clawback 컬럼 V114 추가)
* award_amount = base_amount × award_rate (RATE) or award_fixed (FIXED)
* net_amount = award_amount - clawback_amount
*/
@Data
public class AwardLedgerVO {
@@ -21,7 +22,11 @@ public class AwardLedgerVO {
private BigDecimal baseAmount;
/** 시상금 */
private BigDecimal awardAmount;
/** CALCULATED / CONFIRMED / PAID / CANCELLED */
/** 환수액 (V114) */
private BigDecimal clawbackAmount;
/** 실지급액 = award_amount - clawback_amount (V114) */
private BigDecimal netAmount;
/** CALCULATED / CONFIRMED / PAID / CANCELLED / CLAWED_BACK */
private String status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@@ -6,7 +6,7 @@
<resultMap id="VOMap" type="com.ga.core.vo.commission.AwardLedgerVO"/>
<resultMap id="RespMap" type="com.ga.core.vo.commission.AwardLedgerResp"/>
<!-- 목록/단건 조회 컬럼 — 설계사명·조직명·공통코드명 조인 포함 -->
<!-- 목록/단건 조회 컬럼 — 설계사명·조직명·공통코드명 조인 포함 (V114: clawback_amount, net_amount 추가) -->
<sql id="ledgerJoinCols">
l.ledger_id,
l.agent_id, a.agent_name, o.org_name,
@@ -15,6 +15,7 @@
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'AWARD_TYPE' AND cc.code = l.award_type) AS award_type_name,
l.base_amount, l.award_amount,
l.clawback_amount, l.net_amount,
l.status,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'INCOME_STATUS' AND cc.code = l.status) AS status_name
@@ -54,12 +55,14 @@
AND status != 'CANCELLED'
</select>
<!-- award_amount 는 Service가 계산 후 VO에 set, status 기본 CALCULATED -->
<!-- award_amount/net_amount 는 Service가 계산 후 VO에 set, status 기본 CALCULATED (V114: clawback_amount, net_amount 포함) -->
<insert id="insert" useGeneratedKeys="true" keyProperty="ledgerId">
INSERT INTO award_ledger
(agent_id, settle_month, award_type, base_amount, award_amount, status)
(agent_id, settle_month, award_type, base_amount, award_amount,
clawback_amount, net_amount, status)
VALUES
(#{agentId}, #{settleMonth}, #{awardType}, #{baseAmount}, #{awardAmount},
COALESCE(#{clawbackAmount}, 0), #{netAmount},
COALESCE(#{status}, 'CALCULATED'))
</insert>
@@ -73,14 +76,24 @@
DELETE FROM award_ledger WHERE ledger_id = #{ledgerId}
</delete>
<!-- 정산월 기준 설계사별 시상금 합계 -->
<!-- 정산월 기준 설계사별 시상금 합계 — net_amount 기준(V114: 환수 자동 차감). CANCELLED 제외. -->
<select id="aggregateByAgent" resultType="map">
SELECT agent_id AS "agentId",
SUM(award_amount) AS "total"
SELECT agent_id AS "agentId",
SUM(net_amount) AS "total"
FROM award_ledger
WHERE settle_month = #{settleMonth}
AND status != 'CANCELLED'
GROUP BY agent_id
</select>
<!-- 환수 처리 — clawback_amount/net_amount/status/updated_at 갱신 -->
<update id="applyClawback">
UPDATE award_ledger
SET clawback_amount = #{clawbackAmount},
net_amount = #{netAmount},
status = #{status},
updated_at = NOW()
WHERE ledger_id = #{ledgerId}
</update>
</mapper>