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>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
-- V111: 환수채권 이월/미수금 관리 (Clawback Receivable Carry-forward)
|
||||
-- P9 스펙 §2 전체 구현.
|
||||
-- - clawback_receivable 테이블 (환수채권 마스터)
|
||||
-- - clawback_recovery 테이블 (상계 회수 이력)
|
||||
-- - settle_master 컬럼 3개 추가
|
||||
-- - 메뉴 / 권한 / 공통코드 시드 (V95/V96 패턴 동일, ON CONFLICT DO NOTHING 멱등)
|
||||
|
||||
-- ============================================================
|
||||
-- 1. clawback_receivable (환수채권 마스터)
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE clawback_receivable (
|
||||
receivable_id BIGSERIAL PRIMARY KEY,
|
||||
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
|
||||
origin_settle_month CHAR(6) NOT NULL,
|
||||
original_amount NUMERIC(15,2) NOT NULL,
|
||||
recovered_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||
balance NUMERIC(15,2) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'OUTSTANDING',
|
||||
last_recovery_month CHAR(6),
|
||||
writeoff_reason VARCHAR(300),
|
||||
written_off_at TIMESTAMP,
|
||||
written_off_by BIGINT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT now(),
|
||||
created_by BIGINT,
|
||||
updated_at TIMESTAMP,
|
||||
CONSTRAINT uq_clawback_rcv_agent_month UNIQUE (agent_id, origin_settle_month),
|
||||
CONSTRAINT chk_clawback_rcv_status CHECK (status IN ('OUTSTANDING','RECOVERING','CLEARED','WRITTEN_OFF')),
|
||||
CONSTRAINT chk_clawback_rcv_orig_amt CHECK (original_amount >= 0),
|
||||
CONSTRAINT chk_clawback_rcv_balance CHECK (balance >= 0)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_clawback_rcv_agent_status ON clawback_receivable (agent_id, status);
|
||||
CREATE INDEX idx_clawback_rcv_status ON clawback_receivable (status);
|
||||
CREATE INDEX idx_clawback_rcv_origin_month ON clawback_receivable (origin_settle_month);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. clawback_recovery (상계 회수 이력)
|
||||
-- ============================================================
|
||||
|
||||
CREATE TABLE clawback_recovery (
|
||||
recovery_id BIGSERIAL PRIMARY KEY,
|
||||
receivable_id BIGINT NOT NULL REFERENCES clawback_receivable(receivable_id),
|
||||
settle_month CHAR(6) NOT NULL,
|
||||
recovered_amount NUMERIC(15,2) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_clawback_rec_receivable ON clawback_recovery (receivable_id);
|
||||
CREATE INDEX idx_clawback_rec_month ON clawback_recovery (settle_month);
|
||||
|
||||
-- ============================================================
|
||||
-- 3. settle_master 컬럼 3개 추가
|
||||
-- ============================================================
|
||||
|
||||
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;
|
||||
|
||||
COMMENT ON COLUMN settle_master.clawback_recovered IS '당월 채권 상계 회수액';
|
||||
COMMENT ON COLUMN settle_master.clawback_carryover IS '당월 발생→이월된 미수액';
|
||||
COMMENT ON COLUMN settle_master.payable_amount IS '실지급(이체대상) = max(0,net) − recovered';
|
||||
|
||||
-- ============================================================
|
||||
-- 4. 메뉴 INSERT: CLAWBACK_RECEIVABLE (GRP_COMMISSION 하위 PAGE)
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO menu (parent_menu_id, menu_code, menu_name, menu_type, menu_path, component_path, menu_level, sort_order)
|
||||
SELECT p.menu_id, x.code, x.name, 'PAGE', x.path, x.cmpt, 2, x.sort
|
||||
FROM menu p
|
||||
JOIN (VALUES
|
||||
('CLAWBACK_RECEIVABLE', '환수채권관리', '/commission/clawback-receivable', 'commission/ClawbackReceivable', 150)
|
||||
) AS x(code, name, path, cmpt, sort)
|
||||
ON p.menu_code = 'GRP_COMMISSION'
|
||||
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code);
|
||||
|
||||
-- ============================================================
|
||||
-- 5. 메뉴 권한 항목 등록 (READ/UPDATE/EXPORT)
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
|
||||
SELECT m.menu_id, p.code, p.name, p.sort
|
||||
FROM menu m
|
||||
CROSS JOIN (VALUES
|
||||
('READ', '조회', 10),
|
||||
('UPDATE', '수정', 30),
|
||||
('EXPORT', '엑셀', 60)
|
||||
) AS p(code, name, sort)
|
||||
WHERE m.menu_code = 'CLAWBACK_RECEIVABLE'
|
||||
ON CONFLICT (menu_id, perm_code) DO NOTHING;
|
||||
|
||||
-- ============================================================
|
||||
-- 6. 역할별 권한 부여
|
||||
-- ============================================================
|
||||
|
||||
-- SUPER_ADMIN / ADMIN — 전체 권한 (READ/UPDATE/EXPORT)
|
||||
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
|
||||
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
|
||||
FROM role r
|
||||
CROSS JOIN menu_permission mp
|
||||
JOIN menu m ON m.menu_id = mp.menu_id
|
||||
WHERE r.role_code IN ('SUPER_ADMIN', 'ADMIN')
|
||||
AND m.menu_code = 'CLAWBACK_RECEIVABLE'
|
||||
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||
|
||||
-- MANAGER — READ 만
|
||||
INSERT INTO role_menu_permission (role_id, menu_id, perm_code, is_granted)
|
||||
SELECT r.role_id, mp.menu_id, mp.perm_code, 'Y'
|
||||
FROM role r
|
||||
CROSS JOIN menu_permission mp
|
||||
JOIN menu m ON m.menu_id = mp.menu_id
|
||||
WHERE r.role_code = 'MANAGER'
|
||||
AND mp.perm_code = 'READ'
|
||||
AND m.menu_code = 'CLAWBACK_RECEIVABLE'
|
||||
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||
|
||||
-- ============================================================
|
||||
-- 7. 공통코드: CLAWBACK_RCV_STATUS
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||
('CLAWBACK_RCV_STATUS', '환수채권상태', 'OUTSTANDING/RECOVERING/CLEARED/WRITTEN_OFF', 'Y', 440)
|
||||
ON CONFLICT (group_code) DO NOTHING;
|
||||
|
||||
INSERT INTO common_code (group_code, code, code_name, sort_order) VALUES
|
||||
('CLAWBACK_RCV_STATUS', 'OUTSTANDING', '미회수', 10),
|
||||
('CLAWBACK_RCV_STATUS', 'RECOVERING', '회수중', 20),
|
||||
('CLAWBACK_RCV_STATUS', 'CLEARED', '상환완료', 30),
|
||||
('CLAWBACK_RCV_STATUS', 'WRITTEN_OFF', '대손', 40)
|
||||
ON CONFLICT (group_code, code) DO NOTHING;
|
||||
Reference in New Issue
Block a user