Files
ga-commission-system/docs/DOMAIN_GAP_P9_환수채권이월.md
GA Pro 51ee03453c feat: P9 환수채권 이월/미수금 관리 — 수수료 갭 풀스택 (PL 자율진행)
incar CMS 메뉴(환수이월관리/환수계약리스트/위촉중환수) ↔ GA 부재 + 코드 갭
(AggregateStep net<0 시 경고만, 미회수 환수 채권화·차월상계·대손 생애주기 전무)
을 도출해 풀스택 구현. 미회수 환수가 소실되던 재무 누락을 폐쇄.

- DB(V111): clawback_receivable(채권 마스터, UNIQUE agent+origin월) +
  clawback_recovery(상계 회수 이력) + settle_master 3컬럼(clawback_recovered/
  clawback_carryover/payable_amount) + 메뉴 CLAWBACK_RECEIVABLE + 권한 + 공통코드.
- core: VO/Resp/SaveReq/SearchParam/Enum/Mapper(.java/.xml) 2테이블 + MapStruct.
  outstandingBalance(origin<settleMonth 자기상계방지) / FIFO / upsertCarryover(멱등).
- api: ClawbackReceivableService/Controller(/api/clawback-receivables) — list/detail/
  summary/write-off(OUTSTANDING·RECOVERING만, CLEARED/WRITTEN_OFF 전이)/export(@Transactional).
- batch: AggregateStep 통합 — net<0 → carryover 채권화, net>0 → FIFO 상계회수,
  reverse-먼저 방식으로 동일월 재실행 멱등. net_amount 의미 불변(무결성 보존).
  payable_amount = max(0,net) − recovered.
- fix(잠복): SettleMasterMapper.xml upsert에 신규 3컬럼 누락 → INSERT/ON CONFLICT 추가
  (배치가 VO에 set해도 DEFAULT 0으로 미영속되던 버그, PL 검증서 발견).
- frontend: ClawbackReceivable 화면(AG Grid 합계행 + 회수이력 타임라인 Drawer +
  대손/상환 모달) + api 모듈 + App 라우트.

검증(PL 직접): ./gradlew build(test포함) SUCCESSFUL. Flyway V109~V111 운영DB 적용
(schema v111). 라이브 API: GET 목록/summary 200, write-off WRITTEN_OFF 200(한글사유
영속), 종결 재처리·잘못된 전이상태 E411 차단. 배치 이월/상계/멱등/부분상계 4시나리오
실 스키마 롤백 트랜잭션 검증 PASS(CHECK balance>=0 포함). 잔여 데이터 0.

스펙: docs/DOMAIN_GAP_P9_환수채권이월.md. 후속 갭(시상/인정실적 환수 ledger,
지급대상건 구성, 환수 보험사자료 업로드) 문서화.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 00:18:58 +09:00

9.5 KiB
Raw Permalink Blame History

P9 — 환수채권 이월/미수금 관리 (Clawback Receivable Carry-forward)

작성: PL, 2026-06-04. 사용자 지시 "수수료 빠진 업무 찾아서 기획·설계 후 적용". 근거: incar CMS 메뉴(환수이월관리/환수계약리스트/환수리스트/위촉중환수) ↔ GA 부재 + 코드 갭 (AggregateStep.java:197-199 — net<0 시 경고 로그만, 미회수 환수 채권화·차월상계·대손 생애주기 전무).

범위: 풀스택(DB→Core→API→Batch→Frontend). 패턴 100% 준수(template = 정착지원금 V90 / SettlingSupport*). Flyway: V110이 최신 → V111부터. 금액 NUMERIC(15,2), HALF_UP.

확정 FK: agent(agent_id), settle_master(agent_id, settle_month).


1. 업무 정의 (표준 한국 GA 관행 — 데이터로 외부화)

조기해지 차등환수(chargeback)·공제가 당월 지급수수료(gross)를 초과하면 net < 0. 이 미회수분은 설계사가 GA에 갚아야 할 미수채권(환수채권) 이다. 표준 처리:

  1. 이월(carryover): 당월 net<0 → 음수분(|net|)을 환수채권으로 신규 등록, 당월 실지급(payable)=0.
  2. 상계 회수(recovery): 차월 이후 net>0이면 미결 채권을 FIFO(오래된 origin월 우선) 로 상계 회수. payable = net 회수액.
  3. 대손/직접상환(write-off / direct settle): 장기 미회수 채권은 관리자가 대손(WRITTEN_OFF) 또는 외부 직접상환(CLEARED) 처리.

표준 가정(회사 정책 다르면 데이터/룰만 수정, 코드 불변):

  • 상계 우선순위 = origin_settle_month ASC (FIFO).
  • 회수는 당월 양수 net 한도 내에서만(설계사를 음수로 만들지 않음).
  • 이자·연체료 없음(필요 시 후속).

2. 스키마 (V111)

2-1. clawback_receivable (환수채권 마스터)

컬럼 타입 설명
receivable_id BIGSERIAL PK
agent_id BIGINT NN FK agent
origin_settle_month CHAR(6) NN 미수 발생 정산월
original_amount NUMERIC(15,2) NN 발생액(양수, =당월
recovered_amount NUMERIC(15,2) NN DEFAULT 0 누적 상계 회수액
balance NUMERIC(15,2) NN = original_amount recovered_amount
status VARCHAR(20) NN DEFAULT 'OUTSTANDING' OUTSTANDING/RECOVERING/CLEARED/WRITTEN_OFF
last_recovery_month CHAR(6) 최근 회수 정산월
writeoff_reason VARCHAR(300) 대손/직접상환 사유
written_off_at TIMESTAMP
written_off_by BIGINT
created_at TIMESTAMP NN DEFAULT now()
created_by BIGINT
updated_at TIMESTAMP
  • UNIQUE(agent_id, origin_settle_month) — 설계사·발생월당 1건(배치 upsert 멱등 키)
  • CHECK status IN('OUTSTANDING','RECOVERING','CLEARED','WRITTEN_OFF')
  • CHECK original_amount >= 0, balance >= 0
  • idx: (agent_id, status), (status), (origin_settle_month)

2-2. clawback_recovery (상계 회수 이력)

컬럼 타입 설명
recovery_id BIGSERIAL PK
receivable_id BIGINT NN FK clawback_receivable
settle_month CHAR(6) NN 회수(상계) 정산월
recovered_amount NUMERIC(15,2) NN 이 월 이 채권에서 회수한 액
created_at TIMESTAMP NN DEFAULT now()
  • idx: (receivable_id), (settle_month)
  • 멱등 키: 배치 재실행 시 동일 settle_month 회수분을 reverse(삭제+balance 복원) 후 재적용.

2-3. settle_master 컬럼 추가 (기존 의미 불변)

ALTER TABLE settle_master
  ADD COLUMN clawback_recovered NUMERIC(15,2) NOT NULL DEFAULT 0,  -- 당월 채권 상계 회수액
  ADD COLUMN clawback_carryover NUMERIC(15,2) NOT NULL DEFAULT 0,  -- 당월 발생→이월된 미수액
  ADD COLUMN payable_amount     NUMERIC(15,2) NOT NULL DEFAULT 0;  -- 실지급(이체대상) = max(0,net)  recovered
  • net_amount(=gross−소득세−지방세+부가세−공제) 의미·계산 불변(무결성 Σ원장==Σsettle_master 보존).
  • 실제 이체/출금 가용액은 payable_amount 사용(후속: withdraw 한도 검증을 payable_amount 기준으로 전환은 본 P9 범위 외, 문서화만).

2-4. 메뉴 / 권한 / 공통코드

  • 메뉴 CLAWBACK_RECEIVABLE (환수채권관리), 상위 GRP_COMMISSION, path /commission/clawback-receivable, component commission/ClawbackReceivable.
  • 권한 READ/UPDATE/EXPORT + 역할부여(SUPER_ADMIN·ADMIN 전체, MANAGER READ) — V54/V95 패턴.
  • 공통코드 CLAWBACK_RCV_STATUS: OUTSTANDING=미회수/RECOVERING=회수중/CLEARED=상환완료/WRITTEN_OFF=대손.

3. 배치 통합 (AggregateStep, Step 8) — 핵심

설계사 루프에서 rawNet = tax.netAmount() deductionTotal 계산 직후(현 196행), settle_master.upsert 이전에 삽입:

priorBalance = receivableMapper.outstandingBalance(agentId, settleMonth)
               // SUM(balance) WHERE agent_id=? AND status IN('OUTSTANDING','RECOVERING')
               //                AND origin_settle_month < settleMonth
recovered = 0; carryover = 0; payable = 0
if (rawNet.signum() >= 0) {
    recovered = min(rawNet, priorBalance)     // 양수 net에서 기존 채권 FIFO 상계
    payable   = rawNet - recovered
} else {
    payable   = 0
    carryover = rawNet.negate()               // 음수분 신규 채권화
}
vo.setNetAmount(rawNet)                        // 불변
vo.setClawbackRecovered(recovered)
vo.setClawbackCarryover(carryover)
vo.setPayableAmount(payable)
settleMapper.upsert(vo)

// settle_master upsert 가드(status NOT IN CONFIRMED/PAID)와 동일 행만 채권 부수효과 적용
if (settle_master 행이 갱신가능 상태였다면) {
    clawbackService.applyCarryoverAndRecovery(agentId, settleMonth, carryover, recovered)
}

applyCarryoverAndRecovery (배치 멱등 보장) — ga-batch 내 헬퍼 또는 Mapper 직접:

  1. reverse: 이 settleMonth의 기존 clawback_recovery 행 조회 → 각 receivable.balance += recovered_amount, recovered_amount -= , status 재계산(CLEARED→RECOVERING/OUTSTANDING) → recovery 행 삭제. (재실행 안전)
  2. carryover: carryover>0 → upsert clawback_receivable (agent_id, origin=settleMonth) {original_amount=carryover, balance=carryover, recovered_amount=0, status=OUTSTANDING}. (ON CONFLICT 갱신)
  3. recovery: recovered>0 → outstanding 채권 origin_month ASC FIFO 순회:
    • take = min(recovered_remaining, rcv.balance); rcv.recovered_amount += take; rcv.balance -= take; status = balance==0 ? CLEARED : RECOVERING; last_recovery_month=settleMonth; INSERT clawback_recovery(receivable_id, settleMonth, take); recovered_remaining -= take;
    • recovered_remaining==0 종료.

reverse를 먼저 하므로 동일 월 재정산 시 회수 중복 없음. carryover는 UNIQUE upsert로 멱등. 주의: priorBalance 조회는 origin_settle_month < settleMonth 로 당월 발생분 자기상계 방지.

배치 Step 순서: AggregateStep(8) 내부에서 처리(별도 Step 불필요). 회계분개(8b)·세금은 net_amount 기준 불변.


4. API (ga-api)

ClawbackReceivableService + ClawbackReceivableController (/api/clawback-receivables):

  • GET /api/clawback-receivables — 목록(agentId/status/originMonth 필터, PageHelper, @RequirePermission READ)
  • GET /api/clawback-receivables/{id} — 상세 + 회수이력(clawback_recovery 리스트)
  • GET /api/clawback-receivables/summary — 설계사별 미결 잔액 집계(status OUTSTANDING/RECOVERING)
  • PUT /api/clawback-receivables/{id}/write-off — 대손/직접상환 처리(@RequirePermission UPDATE, @DataChangeLog). body {action: WRITTEN_OFF|CLEARED, reason}. balance→0, status 전이, writeoff_* 기록. OUTSTANDING/RECOVERING만 허용(CLEARED/WRITTEN_OFF 재처리 E411).
  • GET /api/clawback-receivables/export — 엑셀(ExcelService, @Transactional).
  • 표준 ApiResponse/PageResponse, MapStruct, BaseMapper 상속.

회수(recovery)는 배치만 생성(수동 회수 엔드포인트 없음 — 정산 상계로만 발생).


5. Frontend (ga-frontend)

  • src/api/clawbackReceivable.ts — list/detail/summary/writeOff/export.
  • src/pages/commission/ClawbackReceivable.tsx — AG Grid(목록 100건+ / status 컬러칩 / 합계행: original/recovered/balance) + 행 클릭 상세 Drawer(회수이력 타임라인) + 대손/상환 처리 버튼(PermissionButton UPDATE) + 엑셀 export.
  • App.tsx 라우트 commission/clawback-receivable (lazy).

6. 검증 (PL 직접)

  1. ./gradlew build(test 포함) SUCCESSFUL, ga-frontend npm run build PASS.
  2. Flyway V111 success, schema v111. 테이블 2개·컬럼 3개·메뉴 1·권한·공통코드 확인.
  3. GET 3종 + write-off PUT 실호출 5xx 0.
  4. 배치 e2e (핵심): 단일 트랜잭션 롤백 시나리오로
    • (a) 특정 설계사에 chargeback>gross 인 net<0 상황 구성 → 202611 정산 → clawback_carryover 발생 + clawback_receivable OUTSTANDING 1건 + payable=0 확인.
    • (b) 익월 202612 정산(양수 net) → clawback_recovered 상계 + receivable balance 감소/CLEARED + clawback_recovery 행 + payable=netrecovered 확인.
    • (c) 동일월 재실행 → 회수 중복 없음(reverse 멱등) 확인.
    • 부산물 전부 롤백, pre-e2e 복원.

7. 후속 갭 (본 P9 범위 외, 기록)

  • 시상/인정실적 환수 ledger (incar 인정실적수수료환수/신인도입유치자시상환수).
  • 지급대상건 구성·수정(수동 포함/제외 워크플로우, incar 지급대상건구성/수정/조회).
  • 환수 보험사자료 업로드(incar 환수보험사자료 업로드 — 보험사 통보 기반 환수 인입).
  • withdraw 가용한도 검증을 net_amount→payable_amount 로 전환.