feat: 수수료 갭 보완 4종 — 지급보류/환수원천세역분개/연체이자/간이지급명세서 (V124~V126)

감사(서브에이전트 3영역)로 발견한 실제 결함/규제 갭만 선별 보완.

- A 지급보류(HOLD) 지급배치 누락 버그: PaymentBatchItemMapper.insertFromSettleMaster
  status NOT IN('PAID','HOLD') — 보류건이 지급배치에 포함되어 실제 이체되던 결함 수정
- B 환수 원천세 역분개: withholding_tax_adjustment(V124) 멱등 적재 +
  분기 원천세신고 집계 netting(payment UNION ALL adjustment). settle_master 금액흐름 불변
- C 환수채권 연체이자: clawback_receivable.accrued_interest + clawback_interest(V125) +
  ClawbackInterestCalculator(연율/1200), config 기본 0 = 안전폴백, 기존 FIFO 상계가 이자 회수
- D 사업소득 간이지급명세서(월별): commission_statement(MONTHLY) 위 읽기전용 리포트 +
  메뉴 REPORT_SIMPLIFIED_PAYMENT(V126) + 프론트 SimplifiedPaymentReport

검증: ./gradlew build 전모듈 GREEN, 신규 단위테스트 6건, ga-frontend tsc -b PASS,
Flyway V124~V126 운영DB 적용(v126), 적대리뷰 APPROVED(LOW 1건 GREATEST 클램프 즉수정),
B/C/D 라이브DB 검증(롤백) + D admin e2e list/export 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-06-08 21:22:13 +09:00
parent ff6d399dd3
commit fbbabf2d04
29 changed files with 832 additions and 19 deletions
@@ -0,0 +1,32 @@
-- V124: 환수(클로백) 발생 시 기납부 원천세 역분개(환급/조정)
-- gross<0(환수 우위) 월에 원천징수 역분개 레코드를 적재하여
-- 분기 원천세신고(withholding_tax_report) 과대신고를 해소한다.
-- settle_master 금액 흐름은 불변 — 본 테이블은 세무신고 보정 전용(부가 레코드).
-- ============================================================
-- 1. withholding_tax_adjustment (원천세 역분개)
-- ============================================================
CREATE TABLE withholding_tax_adjustment (
adjustment_id BIGSERIAL PRIMARY KEY,
agent_id BIGINT NOT NULL,
settle_month CHAR(6) NOT NULL,
reason VARCHAR(20) NOT NULL DEFAULT 'CHARGEBACK',
base_amount NUMERIC(15,2) NOT NULL, -- 역분개 기준액(=상계되지 않은 |gross| 음수분, 양수 저장)
income_tax_adj NUMERIC(15,2) NOT NULL, -- 소득세 역분개(환급분, 음수)
local_tax_adj NUMERIC(15,2) NOT NULL, -- 지방소득세 역분개(환급분, 음수)
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT uq_wta UNIQUE (agent_id, settle_month, reason),
CONSTRAINT chk_wta_reason CHECK (reason IN ('CHARGEBACK')),
CONSTRAINT chk_wta_base CHECK (base_amount >= 0)
);
COMMENT ON TABLE withholding_tax_adjustment IS '환수 원천세 역분개(세무신고 보정 전용)';
COMMENT ON COLUMN withholding_tax_adjustment.agent_id IS '설계사 ID';
COMMENT ON COLUMN withholding_tax_adjustment.settle_month IS '역분개 발생 정산월(YYYYMM)';
COMMENT ON COLUMN withholding_tax_adjustment.reason IS '사유(CHARGEBACK)';
COMMENT ON COLUMN withholding_tax_adjustment.base_amount IS '역분개 기준액(상계되지 않은 |gross| 음수분)';
COMMENT ON COLUMN withholding_tax_adjustment.income_tax_adj IS '소득세 역분개(환급분, 음수)';
COMMENT ON COLUMN withholding_tax_adjustment.local_tax_adj IS '지방소득세 역분개(환급분, 음수)';
CREATE INDEX idx_wta_settle_month ON withholding_tax_adjustment (settle_month);
@@ -0,0 +1,42 @@
-- V125: 환수채권 연체이자 (overdue interest on clawback receivables)
-- 미수 환수채권이 이월될 때 연체기간만큼 단리 연체이자를 월별 적립한다.
-- 이자율 0(기본값) 이면 적립 없음 — 기존 동작과 동일(안전 폴백).
-- ============================================================
-- 1. clawback_receivable 누적 연체이자 컬럼
-- ============================================================
ALTER TABLE clawback_receivable
ADD COLUMN accrued_interest NUMERIC(15,2) NOT NULL DEFAULT 0;
COMMENT ON COLUMN clawback_receivable.accrued_interest IS '누적 연체이자 (단리, balance 에 포함되어 회수/상계됨)';
-- ============================================================
-- 2. clawback_interest (월별 연체이자 적립 이력 — 멱등 reverse용)
-- ============================================================
CREATE TABLE clawback_interest (
interest_id BIGSERIAL PRIMARY KEY,
receivable_id BIGINT NOT NULL REFERENCES clawback_receivable(receivable_id),
settle_month CHAR(6) NOT NULL,
interest_amount NUMERIC(15,2) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT uq_clawback_interest UNIQUE (receivable_id, settle_month)
);
COMMENT ON TABLE clawback_interest IS '환수채권 월별 연체이자 적립 이력';
COMMENT ON COLUMN clawback_interest.receivable_id IS '환수채권 ID';
COMMENT ON COLUMN clawback_interest.settle_month IS '적립 정산월(YYYYMM)';
COMMENT ON COLUMN clawback_interest.interest_amount IS '당월 적립 연체이자(원금잔액 × 월이자율)';
CREATE INDEX idx_clawback_interest_settle_month ON clawback_interest (settle_month);
-- ============================================================
-- 3. system_config 시드 — 연체이자율 / 거치기간
-- 기본값 0 → 적립 없음(기존 동작 보존). 운영정책에 맞게 수정.
-- ============================================================
INSERT INTO system_config (config_key, config_value, config_group, value_type, description) VALUES
('CLAWBACK_OVERDUE_INTEREST_RATE', '0', 'COMMISSION', 'NUMBER', '환수채권 연 연체이자율(%) — 0이면 미적립'),
('CLAWBACK_INTEREST_GRACE_MONTHS', '0', 'COMMISSION', 'NUMBER', '환수채권 연체이자 거치기간(개월) — 발생 후 N개월 경과부터 적립')
ON CONFLICT (config_key) DO NOTHING;
@@ -0,0 +1,43 @@
-- V126: 사업소득 간이지급명세서(월별) 리포트 메뉴
-- REPORT / GRP_REPORT 그룹에 PAGE 추가 (V38 패턴 동일). 신규 데이터 테이블 없음(읽기전용 집계).
-- 1. REPORT 그룹 하위 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
('REPORT_SIMPLIFIED_PAYMENT', '간이지급명세서', '/report/simplified-payment', 'report/SimplifiedPaymentReport', 40)
) AS x(code, name, path, cmpt, sort)
ON p.menu_code = 'REPORT'
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code);
-- 2. GRP_REPORT 그룹 하위 PAGE (_GRP 접미)
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 || '_GRP', x.name, 'PAGE', x.path, x.cmpt, 2, x.sort
FROM menu p
JOIN (VALUES
('REPORT_SIMPLIFIED_PAYMENT', '간이지급명세서', '/report/simplified-payment', 'report/SimplifiedPaymentReport', 40)
) AS x(code, name, path, cmpt, sort)
ON p.menu_code = 'GRP_REPORT'
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code || '_GRP');
-- 3. 권한 항목 (READ/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),
('EXPORT', '엑셀', 60)
) AS p(code, name, sort)
WHERE m.menu_code IN ('REPORT_SIMPLIFIED_PAYMENT', 'REPORT_SIMPLIFIED_PAYMENT_GRP')
ON CONFLICT (menu_id, perm_code) DO NOTHING;
-- 4. SUPER_ADMIN / ADMIN / MANAGER 권한 부여
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', 'MANAGER')
AND m.menu_code IN ('REPORT_SIMPLIFIED_PAYMENT', 'REPORT_SIMPLIFIED_PAYMENT_GRP')
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;