feat: 수수료 공식 갭 보완 + 8종 신규모듈 풀스택 (V116~V123)
수수료 공식 갭분석(5 병렬감사) 후 확정 버그 3건 수정 + 개념적으로 빠진 모듈 8종 풀스택 구현. [확정 버그 3건] - 1200% 누적한도(checkContractTotal)를 배치 CalcRecruitStep에 연결(기존 1차년 한도만 적용 → 두 한도 중 더 제한적인 값으로 clip+이연). +CalcRecruitStepTest - BatchInstallmentPlanGenerator planMonth 반영(하드코딩 1 제거, API판과 동일). +단위테스트 - PersistencyBonus 임계유지율 게이트 + FIXED 정액 룰 도출. +PersistencyBonusServiceTest [신규 모듈 8종] - MOD-1 도입수수료(recruiter/development) / MOD-2 선지급·차익정산 / MOD-3 환수 분할상환 - MOD-4 익월부활 재지급 / MOD-5 정산대사 입수 / MOD-6 환수 시효 / MOD-7 환수 감면 - MOD-8 채널별 수수료차등(sales_channel) 각 DB(테이블+메뉴+권한+공통코드)→VO/Mapper/Enum→Service/Controller→화면 풀스택. 배치 정산연계: AggregateStep에 도입/익월부활 가산(11종), 선지급 FIFO 상계(멱등 reverse), MOD-8 채널 우선 요율조회(NULL 폴백 하위호환). [검증] - 전체 ./gradlew build(전 모듈+전 테스트) GREEN - Flyway V116~V123 운영DB 적용(schema v115→v123) - 라이브 스모크 8 GET=200, MOD-1 RATE공식 50000 실측, 액션 POST 5xx 0건 - 적대적 코드리뷰 APPROVED(MOD-8 계산기 미연결 버그 발견·수정) 스펙: docs/DOMAIN_GAP_MOD1-8.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -48,3 +48,4 @@ ga-frontend/dist/
|
|||||||
login.json
|
login.json
|
||||||
token.txt
|
token.txt
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
.omc/
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
# DOMAIN_GAP_MOD1-8 — 수수료 갭 8종 신규모듈 권위 스펙
|
||||||
|
|
||||||
|
> 2026-06-06 갭분석 결과 "개념적으로 빠진" 수수료 모듈 8종. 업무 수치는 **데모 기본값**(운영 시 룰테이블 조정).
|
||||||
|
> 모든 레이어 에이전트는 이 문서를 단일 진실원으로 따른다. 테이블/컬럼/Enum/경로/메뉴코드명을 **그대로** 사용.
|
||||||
|
|
||||||
|
## 공통 규약 (기존 패턴 — V92/V95/V96 미러)
|
||||||
|
- PK `BIGSERIAL`, 금액 `NUMERIC(15,2)`, 율 `NUMERIC(7,4)`(0~1 소수), 정산월 `CHAR(6)`(YYYYMM).
|
||||||
|
- 감사: `created_at TIMESTAMP NOT NULL DEFAULT NOW()`, `created_by BIGINT`, `updated_at TIMESTAMP`.
|
||||||
|
- 모든 테이블 `COMMENT ON TABLE/COLUMN`, 인덱스 `idx_<약어>_...`.
|
||||||
|
- 메뉴: `menu`(parent=GRP_*, menu_type='PAGE', menu_level=2) + `menu_permission`(READ/CREATE/UPDATE/DELETE/EXPORT) + `role_menu_permission`(SUPER_ADMIN/ADMIN 전체, MANAGER READ/CREATE/UPDATE/EXPORT, AGENT READ). NOT EXISTS/ON CONFLICT 가드.
|
||||||
|
- 공통코드: `common_code_group` + `common_code`.
|
||||||
|
- core: `XxxVO`(전컬럼)/`XxxResp`(+조인명)/`XxxSaveReq`(입력)/`XxxSearchParam`(extends PageParam) + Mapper(.java/.xml, selectList/selectById/insert/update/delete + 도메인쿼리) + Enum(상태) + MapStruct(필요시). Mapper는 BaseMapper 상속 안 함(개별 정의, 기존 commission Mapper 미러).
|
||||||
|
- api: `XxxService`(@Service @Transactional(readOnly=true), 쓰기 @Transactional) + `XxxController`(@RestController @RequestMapping("/api/...") @RequirePermission, @DataChangeLog 쓰기, ApiResponse/PageResponse 반환). 기존 commission Controller 미러.
|
||||||
|
- frontend: `ga-frontend/src/pages/<area>/Xxx.tsx` + `api/<area>.ts` 함수 + `App.tsx` 라우트. 100건↑/셀편집/합계는 AG Grid, 단순 CRUD는 Ant Design DataTable.
|
||||||
|
- 마이그레이션 번호: MOD-1=V116, MOD-2=V117, MOD-3=V118, MOD-4=V119, MOD-5=V120, MOD-6=V121, MOD-7=V122, MOD-8=V123. (필요 시 모듈당 2파일 가능 — 예 V116_a 테이블 / 단일 파일에 메뉴·코드 포함 권장)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MOD-1 — 슈퍼바이저/도입수수료 (recruiter/development commission) [V116]
|
||||||
|
설계사를 GA에 **도입(리크루팅)**한 상위 설계사에게 지급하는 별도 트랙(오버라이드와 무관, recruiter 관계 기반).
|
||||||
|
|
||||||
|
### DB V116
|
||||||
|
- `ALTER TABLE agent ADD COLUMN recruiter_agent_id BIGINT REFERENCES agent(agent_id);` (도입자, NULL 허용)
|
||||||
|
- `recruiter_commission_rule`(rule_id PK, recruiter_type VARCHAR(20) CHECK IN('NEW_RECRUIT','DEVELOPMENT','TRAINING'), calc_type VARCHAR(10) CHECK IN('RATE','FIXED'), commission_rate NUMERIC(7,4), fixed_amount NUMERIC(15,2), effective_from DATE NOT NULL, effective_to DATE, 감사컬럼). CHECK rate 0~1.
|
||||||
|
- `recruiter_commission_ledger`(ledger_id PK, recruiter_agent_id BIGINT NOT NULL REFERENCES agent, recruit_agent_id BIGINT NOT NULL REFERENCES agent, settle_month CHAR(6) NOT NULL, recruiter_type VARCHAR(20) NOT NULL, base_amount NUMERIC(15,2) NOT NULL, commission_amount NUMERIC(15,2) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED' CHECK IN('CALCULATED','CONFIRMED','PAID','CANCELLED'), 감사). idx (recruiter_agent_id, settle_month).
|
||||||
|
- 메뉴 `RECRUITER_COMM`('도입수수료', '/commission/recruiter', 'commission/RecruiterCommission', GRP_COMMISSION, sort 150) + 권한 + 공통코드 `RECRUITER_TYPE`(NEW_RECRUIT 신인도입/DEVELOPMENT 육성/TRAINING 교육).
|
||||||
|
- 공식(데모): commission = base_amount × rule.commission_rate (RATE) / fixed_amount (FIXED). base_amount=도입 설계사의 당월 모집수수료 합(데모 입력 허용).
|
||||||
|
|
||||||
|
### core/api
|
||||||
|
- `RecruiterCommissionRule*` + `RecruiterCommissionLedger*` VO/Resp/SaveReq/SearchParam, Mapper(.java/.xml: selectList/selectById/insert/update/delete + `aggregateByAgent(settleMonth)` = SUM(commission_amount) GROUP BY recruiter_agent_id). Enum `RecruiterType`.
|
||||||
|
- `RecruiterCommissionService`: rule CRUD + ledger list/detail/createLedger(룰 calc_type 권위, ReferralCommissionService 미러) + aggregate. Controller `/api/recruiter-commissions` (rules/ledgers).
|
||||||
|
- **정산 반영**: `AggregateStep`에 recruiterCommissionMapper.aggregateByAgent 추가 → other_commission_total 합산(기존 9종 패턴 동일, recruiter_agent_id 기준 agentId). (지급성이므로 gross 가산.)
|
||||||
|
|
||||||
|
### frontend
|
||||||
|
- `pages/commission/RecruiterCommission.tsx`(룰 탭 + 원장 탭, 등록 모달), `api/commission.ts`에 함수 추가, App.tsx 라우트.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MOD-2 — 선지급/차익정산 (advance payment recovery + delta settlement) [V117]
|
||||||
|
신입/램프업 설계사에 미리 지급한 **선지급금**을 확정수수료로 우선 회수, 차액만 실지급.
|
||||||
|
|
||||||
|
### DB V117 (GRP_SETTLE)
|
||||||
|
- `advance_payment`(advance_id PK, agent_id BIGINT NOT NULL REFERENCES agent, advance_month CHAR(6) NOT NULL, principal_amount NUMERIC(15,2) NOT NULL CHECK>0, recovered_amount NUMERIC(15,2) NOT NULL DEFAULT 0, balance NUMERIC(15,2) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'OUTSTANDING' CHECK IN('OUTSTANDING','PARTIAL','CLEARED','WAIVED'), memo VARCHAR(200), 감사). CHECK balance>=0. idx(agent_id, status).
|
||||||
|
- `advance_recovery_ledger`(recovery_id PK, advance_id BIGINT NOT NULL REFERENCES advance_payment, settle_month CHAR(6) NOT NULL, recovery_amount NUMERIC(15,2) NOT NULL CHECK>0, 감사). idx(advance_id), idx(settle_month).
|
||||||
|
- 메뉴 `ADVANCE_PAYMENT`('선지급정산','/settle/advance-payment','settle/AdvancePayment', GRP_SETTLE, sort 적절) + 권한 + 공통코드 `ADVANCE_STATUS`(OUTSTANDING 미상환/PARTIAL 부분/CLEARED 완료/WAIVED 면제).
|
||||||
|
|
||||||
|
### core/api/batch
|
||||||
|
- `AdvancePayment*` + `AdvanceRecoveryLedger*` VO/Resp/SaveReq/SearchParam, Mapper. 도메인쿼리: `outstandingByAgent(agentId)`=SUM(balance) WHERE status IN('OUTSTANDING','PARTIAL'); aging은 advance_month 기준.
|
||||||
|
- `AdvancePaymentService`: 선지급 등록(balance=principal)/조회/aging 리포트/면제(waive). Controller `/api/advance-payments`.
|
||||||
|
- **정산 연계(AggregateStep)**: net(=tax후) 계산 직후, P9 환수상계와 **동일 위치/유사 패턴**으로 선지급 잔액 FIFO 우선상계. recovered_advance = min(payable, outstanding). 차익정산 delta = payable − recovered_advance. payable 최종 = delta. advance_recovery_ledger 기록 + advance_payment.recovered/balance/status 갱신. **멱등**: 당월 recovery reverse 후 재적용(P9 reverseClawbackRecovery 패턴). 환수상계와 선지급상계 순서: 환수(채권) → 선지급. 둘 다 payable 차감.
|
||||||
|
- 주의: 기존 settle_master payable_amount 의미에 선지급 차감 반영. 새 컬럼 불필요(payable에 통합) — 단 advance_recovery_ledger로 추적.
|
||||||
|
|
||||||
|
### frontend
|
||||||
|
- `pages/settle/AdvancePayment.tsx`(선지급 목록+잔액/상태, 등록·면제 모달, 상계내역 Drawer), `api/settle.ts` 또는 `api/operation.ts` 함수.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MOD-3 — 환수 분할상환 (installment clawback) [V118]
|
||||||
|
대액 환수채권을 N개월 분할 납부 계획으로.
|
||||||
|
|
||||||
|
### DB V118 (GRP_COMMISSION)
|
||||||
|
- `clawback_installment_plan`(plan_id PK, receivable_id BIGINT NOT NULL REFERENCES clawback_receivable(receivable_id), installment_no INT NOT NULL, installment_amount NUMERIC(15,2) NOT NULL CHECK>0, due_month CHAR(6) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'SCHEDULED' CHECK IN('SCHEDULED','RECOVERED','CANCELLED'), recovered_at TIMESTAMP, 감사). UNIQUE(receivable_id, installment_no). idx(due_month, status).
|
||||||
|
- 메뉴 `CLAWBACK_INSTALLMENT`('환수분할상환','/commission/clawback-installment','commission/ClawbackInstallment', GRP_COMMISSION, sort) + 권한. 공통코드 `CB_INSTALLMENT_STATUS`(SCHEDULED 예정/RECOVERED 회수/CANCELLED 취소).
|
||||||
|
|
||||||
|
### core/api
|
||||||
|
- `ClawbackInstallmentPlan*` VO/Resp/SaveReq/SearchParam + Mapper(selectByReceivable, insertBatch, updateStatus).
|
||||||
|
- `ClawbackInstallmentService`: receivable 1건 + 회차수 N → 균등분할(마지막 회차 잔차흡수, InstallmentPlanGenerator 잔차패턴) 계획 생성. due_month=기준월+회차. list/detail/generate/cancel. Controller `/api/clawback-installments`.
|
||||||
|
- 월별 상계 연계는 데모 수준(계획 생성·조회·상태전이). (AggregateStep 통합은 선택 — 미연계 시 문서화.)
|
||||||
|
|
||||||
|
### frontend
|
||||||
|
- `pages/commission/ClawbackInstallment.tsx`(채권 선택→분할 생성, 회차 목록), `api/commission.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MOD-4 — 익월 부활 재지급 (lapsed reinstatement repayment) [V119]
|
||||||
|
실효(보류)된 계약이 익월 부활 시 보류했던 수수료를 make-good 재지급.
|
||||||
|
|
||||||
|
### DB V119 (GRP_COMMISSION)
|
||||||
|
- `lapsed_reinstatement_recovery`(recovery_id PK, contract_id BIGINT NOT NULL REFERENCES contract(contract_id), agent_id BIGINT NOT NULL REFERENCES agent, lapsed_month CHAR(6) NOT NULL, reinstated_month CHAR(6) NOT NULL, withheld_amount NUMERIC(15,2) NOT NULL CHECK>=0, repaid_amount NUMERIC(15,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'PENDING' CHECK IN('PENDING','REPAID','CANCELLED'), 감사). idx(agent_id, reinstated_month), idx(contract_id).
|
||||||
|
- 메뉴 `LAPSED_REINSTATEMENT`('익월부활재지급','/commission/lapsed-reinstatement','commission/LapsedReinstatement', GRP_COMMISSION, sort) + 권한. 공통코드 `LAPSE_RECOVERY_STATUS`(PENDING 대기/REPAID 재지급완료/CANCELLED 취소).
|
||||||
|
|
||||||
|
### core/api
|
||||||
|
- `LapsedReinstatementRecovery*` VO/Resp/SaveReq/SearchParam + Mapper(selectList/selectById/insert/updateStatus + aggregateByAgent=SUM(repaid_amount) WHERE status='REPAID' GROUP BY agent_id).
|
||||||
|
- `LapsedReinstatementService`: 부활 등록(withheld_amount=보류액, 데모 입력)/repay(상태 REPAID, repaid=withheld)/cancel. Controller `/api/lapsed-reinstatements`.
|
||||||
|
- **정산 반영(선택)**: aggregateByAgent를 AggregateStep other_commission_total에 가산(REPAID만). 미연계 시 문서화.
|
||||||
|
|
||||||
|
### frontend
|
||||||
|
- `pages/commission/LapsedReinstatement.tsx`, `api/commission.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MOD-5 — 정산대사 입수 (carrier reconciliation IN) [V120]
|
||||||
|
보험사 통보 정산자료 vs GA 원장 대사 차이 산출.
|
||||||
|
|
||||||
|
### DB V120 (GRP_SETTLE)
|
||||||
|
- `carrier_reconciliation_in`(recon_id PK, company_id BIGINT NOT NULL REFERENCES insurance_company(company_id), settle_month CHAR(6) NOT NULL, file_name VARCHAR(200), record_count INT NOT NULL DEFAULT 0, reported_total NUMERIC(15,2) NOT NULL DEFAULT 0, system_total NUMERIC(15,2) NOT NULL DEFAULT 0, variance_amount NUMERIC(15,2) NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'RECEIVED' CHECK IN('RECEIVED','MATCHED','VARIANCE','APPROVED'), 감사). UNIQUE(company_id, settle_month). idx(settle_month).
|
||||||
|
- `reconciliation_variance`(variance_id PK, recon_id BIGINT NOT NULL REFERENCES carrier_reconciliation_in, policy_no VARCHAR(50), expected_amount NUMERIC(15,2), reported_amount NUMERIC(15,2), variance_reason VARCHAR(100), 감사). idx(recon_id).
|
||||||
|
- 메뉴 `CARRIER_RECON_IN`('정산대사입수','/settle/carrier-recon-in','settle/CarrierReconIn', GRP_SETTLE, sort) + 권한. 공통코드 `RECON_IN_STATUS`(RECEIVED 입수/MATCHED 일치/VARIANCE 차이/APPROVED 승인).
|
||||||
|
- insurance_company PK 컬럼명 확인 후 사용(company_id 가정, 다르면 실제명).
|
||||||
|
|
||||||
|
### core/api
|
||||||
|
- `CarrierReconciliationIn*` + `ReconciliationVariance*` VO/Resp/SaveReq/SearchParam + Mapper.
|
||||||
|
- `CarrierReconciliationInService`: 대사 등록(reported_total 입력)/reconcile(system_total=settle_master 또는 income_commission 집계, variance=reported−system, status 자동 MATCHED/VARIANCE)/approve. Controller `/api/carrier-recon-in`.
|
||||||
|
|
||||||
|
### frontend
|
||||||
|
- `pages/settle/CarrierReconIn.tsx`(대사 목록 + 차이내역 Drawer), `api/settle.ts`/`operation.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MOD-6 — 환수 시효/소멸 (statute of limitations) [V121]
|
||||||
|
오래된 미결 환수채권을 만료개월 경과 시 자동 대손(WRITTEN_OFF/EXPIRED).
|
||||||
|
|
||||||
|
### DB V121 (GRP_COMMISSION)
|
||||||
|
- `statute_config`(config_id PK, entity_type VARCHAR(30) NOT NULL UNIQUE CHECK IN('CLAWBACK_RECEIVABLE'), expiration_months INT NOT NULL CHECK>0, is_active CHAR(1) NOT NULL DEFAULT 'Y', 감사). 시드 1행: ('CLAWBACK_RECEIVABLE', 36, 'Y').
|
||||||
|
- 메뉴 `STATUTE_CONFIG`('환수시효관리','/commission/statute','commission/StatuteConfig', GRP_COMMISSION, sort) + 권한.
|
||||||
|
- (별도 status 코드 불필요 — 기존 clawback_receivable status에 WRITTEN_OFF 존재 가정. 없으면 사유코드만 추가.)
|
||||||
|
|
||||||
|
### core/api
|
||||||
|
- `StatuteConfig*` VO/Resp/SaveReq + Mapper. clawback_receivable에 만료대상 조회/대손처리 쿼리(`selectExpired(beforeMonth)`, `writeOffExpired`) 추가(기존 ClawbackReceivableMapper에).
|
||||||
|
- `StatuteService`: config CRUD + `expireOverdue(baseMonth)` = origin_settle_month <= baseMonth−expiration_months AND status='OUTSTANDING' → WRITTEN_OFF(reason EXPIRED) + 카운트. Controller `/api/statute` (config + POST expire).
|
||||||
|
|
||||||
|
### frontend
|
||||||
|
- `pages/commission/StatuteConfig.tsx`(설정 + 만료실행 버튼 + 결과), `api/commission.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MOD-7 — 환수 감면/면제 (chargeback waiver/reduction) [V122]
|
||||||
|
재직 설계사 환수에 대한 감면 신청·승인 워크플로.
|
||||||
|
|
||||||
|
### DB V122 (GRP_COMMISSION)
|
||||||
|
- `chargeback_waiver_request`(request_id PK, contract_id BIGINT NOT NULL REFERENCES contract, agent_id BIGINT NOT NULL REFERENCES agent, settle_month CHAR(6) NOT NULL, original_chargeback NUMERIC(15,2) NOT NULL CHECK>=0, waiver_amount NUMERIC(15,2) NOT NULL CHECK>=0, reason VARCHAR(200), approval_status VARCHAR(20) NOT NULL DEFAULT 'REQUESTED' CHECK IN('REQUESTED','APPROVED','REJECTED'), approver_id BIGINT, approved_at TIMESTAMP, 감사). CHECK waiver_amount<=original_chargeback. idx(agent_id, settle_month).
|
||||||
|
- 메뉴 `CHARGEBACK_WAIVER`('환수감면신청','/commission/chargeback-waiver','commission/ChargebackWaiver', GRP_COMMISSION, sort) + 권한. 공통코드 `WAIVER_STATUS`(REQUESTED 신청/APPROVED 승인/REJECTED 반려).
|
||||||
|
|
||||||
|
### core/api
|
||||||
|
- `ChargebackWaiverRequest*` VO/Resp/SaveReq/SearchParam + Mapper.
|
||||||
|
- `ChargebackWaiverService`: 신청(create)/approve(승인 시 approval_status, approver/approved_at; 환수액 조정은 데모 수준 — 승인분 추적)/reject. waiver<=original 검증. Controller `/api/chargeback-waivers`.
|
||||||
|
|
||||||
|
### frontend
|
||||||
|
- `pages/commission/ChargebackWaiver.tsx`(신청 목록 + 승인/반려 액션), `api/commission.ts`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## MOD-8 — 채널별 수수료 차등 (channel-specific commission) [V123]
|
||||||
|
판매채널(직판/방카/홈쇼핑/제휴)별 요율 차등. **하위호환 필수**(channel NULL = 기존 동작).
|
||||||
|
|
||||||
|
### DB V123
|
||||||
|
- `ALTER TABLE contract ADD COLUMN sales_channel VARCHAR(20);` (NULL 허용, NULL=DIRECT 취급)
|
||||||
|
- 공통코드 `SALES_CHANNEL`(DIRECT 직판/BANCASSURANCE 방카/HOME_SHOPPING 홈쇼핑/PARTNERSHIP 제휴).
|
||||||
|
- commission_rate / payout_rule 룰 테이블에 `sales_channel VARCHAR(20)` 컬럼 추가(NULL=모든채널). **실제 룰 테이블명·조회 SQL 확인 필수**(CommissionCalculator가 쓰는 rate 조회). 조회 우선순위: channel 일치 룰 우선, 없으면 channel IS NULL 룰 폴백.
|
||||||
|
- 메뉴 신규 불필요(계약 화면에서 노출). 단 계약 등록/조회에 sales_channel 추가.
|
||||||
|
|
||||||
|
### core/api/batch
|
||||||
|
- ContractVO/Resp/SaveReq에 salesChannel. ContractMapper.xml insert/update/select에 sales_channel.
|
||||||
|
- rate 조회 Mapper SQL에 channel 파라미터 추가(`AND (sales_channel = #{channel} OR sales_channel IS NULL) ORDER BY sales_channel NULLS LAST` 패턴으로 channel 우선). CommissionCalculator가 contract.salesChannel 전달.
|
||||||
|
- **하위호환 회귀 필수 검증**: channel NULL 계약은 기존과 동일 rate. 기존 CommissionCalculatorTest 통과 유지.
|
||||||
|
|
||||||
|
### frontend
|
||||||
|
- 계약 페이지(`pages/contracts/...` 또는 product)에서 sales_channel select 노출(공통코드 SALES_CHANNEL).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 실행 순서 (5-Wave, 레이어별 병렬)
|
||||||
|
1. **DBA**: V116~V123 작성 (dba-architect). → SQL 파싱/리뷰.
|
||||||
|
2. **CORE**: 8모듈 VO/Mapper/Enum/MapStruct (core-architect). → `:ga-core:compileJava` GREEN.
|
||||||
|
3. **API+BATCH**: Service/Controller (api-developer) ‖ AggregateStep/배치 연계 (batch-engineer). → `:ga-api:compileJava :ga-batch:compileJava` GREEN.
|
||||||
|
4. **FRONTEND**: 화면+api+라우트 (frontend-developer). → `tsc` GREEN.
|
||||||
|
5. **INFRA/검증**: `./gradlew build`(test) GREEN + Flyway 적용 + 스모크 (PL + infra-reviewer).
|
||||||
|
- 각 Wave 후 PL이 빌드로 직접 검증. 컬럼/이름 불일치는 PL이 즉시 수정.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.ga.api.controller.commission;
|
||||||
|
|
||||||
|
import com.ga.api.service.commission.AdvancePaymentService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.commission.AdvancePaymentResp;
|
||||||
|
import com.ga.core.vo.commission.AdvancePaymentSaveReq;
|
||||||
|
import com.ga.core.vo.commission.AdvancePaymentSearchParam;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Tag(name = "선지급정산")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/advance-payments")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AdvancePaymentController {
|
||||||
|
|
||||||
|
private final AdvancePaymentService service;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<AdvancePaymentResp>> list(
|
||||||
|
@ModelAttribute AdvancePaymentSearchParam param) {
|
||||||
|
return ApiResponse.ok(service.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{advanceId}")
|
||||||
|
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "READ")
|
||||||
|
public ApiResponse<AdvancePaymentResp> detail(@PathVariable Long advanceId) {
|
||||||
|
return ApiResponse.ok(service.detail(advanceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "ADVANCE_PAYMENT", table = "advance_payment")
|
||||||
|
public ApiResponse<Long> create(@Valid @RequestBody AdvancePaymentSaveReq req) {
|
||||||
|
return ApiResponse.ok(service.create(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{advanceId}/waive")
|
||||||
|
@RequirePermission(menu = "ADVANCE_PAYMENT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "ADVANCE_PAYMENT", table = "advance_payment")
|
||||||
|
public ApiResponse<Void> waive(@PathVariable Long advanceId) {
|
||||||
|
service.waive(advanceId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package com.ga.api.controller.commission;
|
||||||
|
|
||||||
|
import com.ga.api.service.commission.CarrierReconciliationInService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.commission.CarrierReconciliationInResp;
|
||||||
|
import com.ga.core.vo.commission.CarrierReconciliationInSaveReq;
|
||||||
|
import com.ga.core.vo.commission.CarrierReconciliationInSearchParam;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Tag(name = "정산대사입수")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/carrier-recon-in")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class CarrierReconciliationInController {
|
||||||
|
|
||||||
|
private final CarrierReconciliationInService service;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<CarrierReconciliationInResp>> list(
|
||||||
|
@ModelAttribute CarrierReconciliationInSearchParam param) {
|
||||||
|
return ApiResponse.ok(service.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{reconId}")
|
||||||
|
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "READ")
|
||||||
|
public ApiResponse<CarrierReconciliationInResp> detail(@PathVariable Long reconId) {
|
||||||
|
return ApiResponse.ok(service.detail(reconId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "CARRIER_RECON_IN", table = "carrier_reconciliation_in")
|
||||||
|
public ApiResponse<Long> create(@Valid @RequestBody CarrierReconciliationInSaveReq req) {
|
||||||
|
return ApiResponse.ok(service.create(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{reconId}/reconcile")
|
||||||
|
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "CARRIER_RECON_IN", table = "carrier_reconciliation_in")
|
||||||
|
public ApiResponse<Void> reconcile(@PathVariable Long reconId) {
|
||||||
|
service.reconcile(reconId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{reconId}/approve")
|
||||||
|
@RequirePermission(menu = "CARRIER_RECON_IN", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "CARRIER_RECON_IN", table = "carrier_reconciliation_in")
|
||||||
|
public ApiResponse<Void> approve(@PathVariable Long reconId) {
|
||||||
|
service.approve(reconId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package com.ga.api.controller.commission;
|
||||||
|
|
||||||
|
import com.ga.api.service.commission.ChargebackWaiverService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.commission.ChargebackWaiverRequestResp;
|
||||||
|
import com.ga.core.vo.commission.ChargebackWaiverRequestSaveReq;
|
||||||
|
import com.ga.core.vo.commission.ChargebackWaiverRequestSearchParam;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Tag(name = "환수감면신청")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/chargeback-waivers")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ChargebackWaiverController {
|
||||||
|
|
||||||
|
private final ChargebackWaiverService service;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<ChargebackWaiverRequestResp>> list(
|
||||||
|
@ModelAttribute ChargebackWaiverRequestSearchParam param) {
|
||||||
|
return ApiResponse.ok(service.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{requestId}")
|
||||||
|
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "READ")
|
||||||
|
public ApiResponse<ChargebackWaiverRequestResp> detail(@PathVariable Long requestId) {
|
||||||
|
return ApiResponse.ok(service.detail(requestId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "CHARGEBACK_WAIVER", table = "chargeback_waiver_request")
|
||||||
|
public ApiResponse<Long> create(@Valid @RequestBody ChargebackWaiverRequestSaveReq req) {
|
||||||
|
return ApiResponse.ok(service.create(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{requestId}/approve")
|
||||||
|
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "CHARGEBACK_WAIVER", table = "chargeback_waiver_request")
|
||||||
|
public ApiResponse<Void> approve(@PathVariable Long requestId) {
|
||||||
|
service.approve(requestId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{requestId}/reject")
|
||||||
|
@RequirePermission(menu = "CHARGEBACK_WAIVER", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "CHARGEBACK_WAIVER", table = "chargeback_waiver_request")
|
||||||
|
public ApiResponse<Void> reject(@PathVariable Long requestId) {
|
||||||
|
service.reject(requestId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package com.ga.api.controller.commission;
|
||||||
|
|
||||||
|
import com.ga.api.service.commission.ClawbackInstallmentService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.commission.ClawbackInstallmentPlanResp;
|
||||||
|
import com.ga.core.vo.commission.ClawbackInstallmentPlanSearchParam;
|
||||||
|
import com.ga.core.vo.commission.ClawbackInstallmentPlanVO;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Tag(name = "환수분할상환")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/clawback-installments")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ClawbackInstallmentController {
|
||||||
|
|
||||||
|
private final ClawbackInstallmentService service;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<ClawbackInstallmentPlanResp>> list(
|
||||||
|
@ModelAttribute ClawbackInstallmentPlanSearchParam param) {
|
||||||
|
return ApiResponse.ok(service.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{planId}")
|
||||||
|
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "READ")
|
||||||
|
public ApiResponse<ClawbackInstallmentPlanResp> detail(@PathVariable Long planId) {
|
||||||
|
return ApiResponse.ok(service.detail(planId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/by-receivable/{receivableId}")
|
||||||
|
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "READ")
|
||||||
|
public ApiResponse<List<ClawbackInstallmentPlanVO>> listByReceivable(
|
||||||
|
@PathVariable Long receivableId) {
|
||||||
|
return ApiResponse.ok(service.listByReceivable(receivableId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 분할상환 계획 생성.
|
||||||
|
* Request body: { "count": N, "baseMonth": "YYYYMM" }
|
||||||
|
*/
|
||||||
|
@PostMapping("/generate/{receivableId}")
|
||||||
|
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "CLAWBACK_INSTALLMENT", table = "clawback_installment_plan")
|
||||||
|
public ApiResponse<Void> generate(@PathVariable Long receivableId,
|
||||||
|
@RequestBody Map<String, Object> body) {
|
||||||
|
int count = Integer.parseInt(body.get("count").toString());
|
||||||
|
String baseMonth = body.get("baseMonth").toString();
|
||||||
|
service.generate(receivableId, count, baseMonth);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{planId}/cancel")
|
||||||
|
@RequirePermission(menu = "CLAWBACK_INSTALLMENT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "CLAWBACK_INSTALLMENT", table = "clawback_installment_plan")
|
||||||
|
public ApiResponse<Void> cancel(@PathVariable Long planId) {
|
||||||
|
service.cancel(planId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package com.ga.api.controller.commission;
|
||||||
|
|
||||||
|
import com.ga.api.service.commission.LapsedReinstatementService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.commission.LapsedReinstatementRecoveryResp;
|
||||||
|
import com.ga.core.vo.commission.LapsedReinstatementRecoverySaveReq;
|
||||||
|
import com.ga.core.vo.commission.LapsedReinstatementRecoverySearchParam;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Tag(name = "익월부활재지급")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/lapsed-reinstatements")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class LapsedReinstatementController {
|
||||||
|
|
||||||
|
private final LapsedReinstatementService service;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<LapsedReinstatementRecoveryResp>> list(
|
||||||
|
@ModelAttribute LapsedReinstatementRecoverySearchParam param) {
|
||||||
|
return ApiResponse.ok(service.list(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{recoveryId}")
|
||||||
|
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "READ")
|
||||||
|
public ApiResponse<LapsedReinstatementRecoveryResp> detail(@PathVariable Long recoveryId) {
|
||||||
|
return ApiResponse.ok(service.detail(recoveryId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "LAPSED_REINSTATEMENT", table = "lapsed_reinstatement_recovery")
|
||||||
|
public ApiResponse<Long> create(@Valid @RequestBody LapsedReinstatementRecoverySaveReq req) {
|
||||||
|
return ApiResponse.ok(service.create(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{recoveryId}/repay")
|
||||||
|
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "LAPSED_REINSTATEMENT", table = "lapsed_reinstatement_recovery")
|
||||||
|
public ApiResponse<Void> repay(@PathVariable Long recoveryId) {
|
||||||
|
service.repay(recoveryId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{recoveryId}/cancel")
|
||||||
|
@RequirePermission(menu = "LAPSED_REINSTATEMENT", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "LAPSED_REINSTATEMENT", table = "lapsed_reinstatement_recovery")
|
||||||
|
public ApiResponse<Void> cancel(@PathVariable Long recoveryId) {
|
||||||
|
service.cancel(recoveryId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
package com.ga.api.controller.commission;
|
||||||
|
|
||||||
|
import com.ga.api.service.commission.RecruiterCommissionService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.vo.commission.*;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
@Tag(name = "도입수수료")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/recruiter-commissions")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RecruiterCommissionController {
|
||||||
|
|
||||||
|
private final RecruiterCommissionService service;
|
||||||
|
|
||||||
|
// ── Rule ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@GetMapping("/rules")
|
||||||
|
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<RecruiterCommissionRuleResp>> listRules(
|
||||||
|
@ModelAttribute RecruiterCommissionRuleSearchParam param) {
|
||||||
|
return ApiResponse.ok(service.listRules(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/rules/{ruleId}")
|
||||||
|
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
|
||||||
|
public ApiResponse<RecruiterCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
|
||||||
|
return ApiResponse.ok(service.detailRule(ruleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/rules")
|
||||||
|
@RequirePermission(menu = "RECRUITER_COMM", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_rule")
|
||||||
|
public ApiResponse<Long> createRule(@Valid @RequestBody RecruiterCommissionRuleSaveReq req) {
|
||||||
|
return ApiResponse.ok(service.createRule(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/rules/{ruleId}")
|
||||||
|
@RequirePermission(menu = "RECRUITER_COMM", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_rule")
|
||||||
|
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
|
||||||
|
@Valid @RequestBody RecruiterCommissionRuleSaveReq req) {
|
||||||
|
service.updateRule(ruleId, req);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/rules/{ruleId}")
|
||||||
|
@RequirePermission(menu = "RECRUITER_COMM", perm = "DELETE")
|
||||||
|
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_rule")
|
||||||
|
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
|
||||||
|
service.deleteRule(ruleId);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ledger ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
@GetMapping("/ledgers")
|
||||||
|
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
|
||||||
|
public ApiResponse<PageResponse<RecruiterCommissionLedgerResp>> listLedgers(
|
||||||
|
@ModelAttribute RecruiterCommissionLedgerSearchParam param) {
|
||||||
|
return ApiResponse.ok(service.listLedgers(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/ledgers/{ledgerId}")
|
||||||
|
@RequirePermission(menu = "RECRUITER_COMM", perm = "READ")
|
||||||
|
public ApiResponse<RecruiterCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
|
||||||
|
return ApiResponse.ok(service.detailLedger(ledgerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/ledgers")
|
||||||
|
@RequirePermission(menu = "RECRUITER_COMM", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "RECRUITER_COMM", table = "recruiter_commission_ledger")
|
||||||
|
public ApiResponse<Long> createLedger(@Valid @RequestBody RecruiterCommissionLedgerSaveReq req) {
|
||||||
|
return ApiResponse.ok(service.createLedger(req));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.ga.api.controller.commission;
|
||||||
|
|
||||||
|
import com.ga.api.service.commission.StatuteService;
|
||||||
|
import com.ga.common.annotation.DataChangeLog;
|
||||||
|
import com.ga.common.annotation.RequirePermission;
|
||||||
|
import com.ga.common.model.ApiResponse;
|
||||||
|
import com.ga.core.vo.commission.StatuteConfigResp;
|
||||||
|
import com.ga.core.vo.commission.StatuteConfigSaveReq;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Tag(name = "환수시효관리")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/statute")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class StatuteController {
|
||||||
|
|
||||||
|
private final StatuteService service;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@RequirePermission(menu = "STATUTE_CONFIG", perm = "READ")
|
||||||
|
public ApiResponse<List<StatuteConfigResp>> list() {
|
||||||
|
return ApiResponse.ok(service.listConfigs());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{configId}")
|
||||||
|
@RequirePermission(menu = "STATUTE_CONFIG", perm = "READ")
|
||||||
|
public ApiResponse<StatuteConfigResp> detail(@PathVariable Long configId) {
|
||||||
|
return ApiResponse.ok(service.detailConfig(configId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@RequirePermission(menu = "STATUTE_CONFIG", perm = "CREATE")
|
||||||
|
@DataChangeLog(menu = "STATUTE_CONFIG", table = "statute_config")
|
||||||
|
public ApiResponse<Long> create(@Valid @RequestBody StatuteConfigSaveReq req) {
|
||||||
|
return ApiResponse.ok(service.createConfig(req));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{configId}")
|
||||||
|
@RequirePermission(menu = "STATUTE_CONFIG", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "STATUTE_CONFIG", table = "statute_config")
|
||||||
|
public ApiResponse<Void> update(@PathVariable Long configId,
|
||||||
|
@Valid @RequestBody StatuteConfigSaveReq req) {
|
||||||
|
service.updateConfig(configId, req);
|
||||||
|
return ApiResponse.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 만료 대상 환수채권 일괄 대손 처리.
|
||||||
|
* Request body: { "baseMonth": "YYYYMM" }
|
||||||
|
*/
|
||||||
|
@PostMapping("/expire")
|
||||||
|
@RequirePermission(menu = "STATUTE_CONFIG", perm = "UPDATE")
|
||||||
|
@DataChangeLog(menu = "STATUTE_CONFIG", table = "clawback_receivable")
|
||||||
|
public ApiResponse<Integer> expire(@RequestBody Map<String, String> body) {
|
||||||
|
String baseMonth = body.get("baseMonth");
|
||||||
|
if (baseMonth == null || baseMonth.isBlank()) {
|
||||||
|
baseMonth = com.ga.common.util.DateUtil.getSettleMonth();
|
||||||
|
}
|
||||||
|
return ApiResponse.ok(service.expireOverdue(baseMonth));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package com.ga.api.service.commission;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.mapper.commission.AdvancePaymentMapper;
|
||||||
|
import com.ga.core.vo.commission.AdvancePaymentResp;
|
||||||
|
import com.ga.core.vo.commission.AdvancePaymentSaveReq;
|
||||||
|
import com.ga.core.vo.commission.AdvancePaymentSearchParam;
|
||||||
|
import com.ga.core.vo.commission.AdvancePaymentVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class AdvancePaymentService {
|
||||||
|
|
||||||
|
private final AdvancePaymentMapper mapper;
|
||||||
|
|
||||||
|
public PageResponse<AdvancePaymentResp> list(AdvancePaymentSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(mapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
public AdvancePaymentResp detail(Long advanceId) {
|
||||||
|
AdvancePaymentResp resp = mapper.selectById(advanceId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 선지급 등록 — balance = principal, status = OUTSTANDING.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Long create(AdvancePaymentSaveReq req) {
|
||||||
|
AdvancePaymentVO vo = new AdvancePaymentVO();
|
||||||
|
vo.setAgentId(req.getAgentId());
|
||||||
|
vo.setAdvanceMonth(req.getAdvanceMonth());
|
||||||
|
vo.setPrincipalAmount(req.getPrincipalAmount());
|
||||||
|
vo.setRecoveredAmount(BigDecimal.ZERO);
|
||||||
|
vo.setBalance(req.getPrincipalAmount());
|
||||||
|
vo.setStatus("OUTSTANDING");
|
||||||
|
vo.setMemo(req.getMemo());
|
||||||
|
mapper.insert(vo);
|
||||||
|
return vo.getAdvanceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 선지급 면제 — status = WAIVED, balance = 0.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void waive(Long advanceId) {
|
||||||
|
AdvancePaymentResp resp = mapper.selectById(advanceId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
if ("CLEARED".equals(resp.getStatus()) || "WAIVED".equals(resp.getStatus())) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
AdvancePaymentVO vo = new AdvancePaymentVO();
|
||||||
|
vo.setAdvanceId(advanceId);
|
||||||
|
vo.setAgentId(resp.getAgentId());
|
||||||
|
vo.setAdvanceMonth(resp.getAdvanceMonth());
|
||||||
|
vo.setPrincipalAmount(resp.getPrincipalAmount());
|
||||||
|
vo.setRecoveredAmount(resp.getRecoveredAmount());
|
||||||
|
vo.setBalance(BigDecimal.ZERO);
|
||||||
|
vo.setStatus("WAIVED");
|
||||||
|
vo.setMemo(resp.getMemo());
|
||||||
|
mapper.update(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 설계사의 미상환 잔액 합계 (OUTSTANDING + PARTIAL).
|
||||||
|
*/
|
||||||
|
public BigDecimal outstandingByAgent(Long agentId) {
|
||||||
|
BigDecimal total = mapper.outstandingByAgent(agentId);
|
||||||
|
return total != null ? total : BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
}
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
package com.ga.api.service.commission;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.common.util.SecurityUtil;
|
||||||
|
import com.ga.core.mapper.commission.CarrierReconciliationInMapper;
|
||||||
|
import com.ga.core.mapper.commission.IncomeCommissionLedgerMapper;
|
||||||
|
import com.ga.core.vo.commission.*;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class CarrierReconciliationInService {
|
||||||
|
|
||||||
|
private final CarrierReconciliationInMapper mapper;
|
||||||
|
private final IncomeCommissionLedgerMapper incomeMapper;
|
||||||
|
|
||||||
|
public PageResponse<CarrierReconciliationInResp> list(CarrierReconciliationInSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(mapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
public CarrierReconciliationInResp detail(Long reconId) {
|
||||||
|
CarrierReconciliationInResp resp = mapper.selectById(reconId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 대사 등록 — reported_total 입력, status = RECEIVED.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Long create(CarrierReconciliationInSaveReq req) {
|
||||||
|
CarrierReconciliationInVO vo = new CarrierReconciliationInVO();
|
||||||
|
vo.setCompanyId(req.getCompanyId());
|
||||||
|
vo.setSettleMonth(req.getSettleMonth());
|
||||||
|
vo.setFileName(req.getFileName());
|
||||||
|
vo.setRecordCount(req.getRecordCount() != null ? req.getRecordCount() : 0);
|
||||||
|
vo.setReportedTotal(req.getReportedTotal());
|
||||||
|
vo.setSystemTotal(BigDecimal.ZERO);
|
||||||
|
vo.setVarianceAmount(BigDecimal.ZERO);
|
||||||
|
vo.setStatus("RECEIVED");
|
||||||
|
vo.setCreatedBy(SecurityUtil.getCurrentUserId());
|
||||||
|
mapper.insert(vo);
|
||||||
|
return vo.getReconId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 대사 실행 — system_total = income_commission 집계, variance = reported − system.
|
||||||
|
* |variance| < 1 이면 MATCHED, 아니면 VARIANCE.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void reconcile(Long reconId) {
|
||||||
|
CarrierReconciliationInResp resp = mapper.selectById(reconId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
if ("APPROVED".equals(resp.getStatus())) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
|
||||||
|
BigDecimal systemTotal = incomeMapper.sumByCarrierAndMonth(
|
||||||
|
resp.getCompanyId(), resp.getSettleMonth());
|
||||||
|
if (systemTotal == null) systemTotal = BigDecimal.ZERO;
|
||||||
|
|
||||||
|
BigDecimal variance = resp.getReportedTotal().subtract(systemTotal);
|
||||||
|
String newStatus = variance.abs().compareTo(BigDecimal.ONE) < 0 ? "MATCHED" : "VARIANCE";
|
||||||
|
|
||||||
|
CarrierReconciliationInVO vo = buildVO(resp);
|
||||||
|
vo.setSystemTotal(systemTotal);
|
||||||
|
vo.setVarianceAmount(variance);
|
||||||
|
vo.setStatus(newStatus);
|
||||||
|
mapper.update(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 승인 처리 — status = APPROVED.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void approve(Long reconId) {
|
||||||
|
CarrierReconciliationInResp resp = mapper.selectById(reconId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
if ("RECEIVED".equals(resp.getStatus())) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
CarrierReconciliationInVO vo = buildVO(resp);
|
||||||
|
vo.setStatus("APPROVED");
|
||||||
|
mapper.update(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CarrierReconciliationInVO buildVO(CarrierReconciliationInResp resp) {
|
||||||
|
CarrierReconciliationInVO vo = new CarrierReconciliationInVO();
|
||||||
|
vo.setReconId(resp.getReconId());
|
||||||
|
vo.setCompanyId(resp.getCompanyId());
|
||||||
|
vo.setSettleMonth(resp.getSettleMonth());
|
||||||
|
vo.setFileName(resp.getFileName());
|
||||||
|
vo.setRecordCount(resp.getRecordCount());
|
||||||
|
vo.setReportedTotal(resp.getReportedTotal());
|
||||||
|
vo.setSystemTotal(resp.getSystemTotal());
|
||||||
|
vo.setVarianceAmount(resp.getVarianceAmount());
|
||||||
|
vo.setStatus(resp.getStatus());
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.ga.api.service.commission;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.common.util.SecurityUtil;
|
||||||
|
import com.ga.core.mapper.commission.ChargebackWaiverRequestMapper;
|
||||||
|
import com.ga.core.vo.commission.*;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class ChargebackWaiverService {
|
||||||
|
|
||||||
|
private final ChargebackWaiverRequestMapper mapper;
|
||||||
|
|
||||||
|
public PageResponse<ChargebackWaiverRequestResp> list(ChargebackWaiverRequestSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(mapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ChargebackWaiverRequestResp detail(Long requestId) {
|
||||||
|
ChargebackWaiverRequestResp resp = mapper.selectById(requestId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 감면 신청 등록 — waiver_amount <= original_chargeback 검증.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Long create(ChargebackWaiverRequestSaveReq req) {
|
||||||
|
if (req.getWaiverAmount().compareTo(req.getOriginalChargeback()) > 0) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
ChargebackWaiverRequestVO vo = new ChargebackWaiverRequestVO();
|
||||||
|
vo.setContractId(req.getContractId());
|
||||||
|
vo.setAgentId(req.getAgentId());
|
||||||
|
vo.setSettleMonth(req.getSettleMonth());
|
||||||
|
vo.setOriginalChargeback(req.getOriginalChargeback());
|
||||||
|
vo.setWaiverAmount(req.getWaiverAmount());
|
||||||
|
vo.setReason(req.getReason());
|
||||||
|
vo.setApprovalStatus("REQUESTED");
|
||||||
|
vo.setCreatedBy(SecurityUtil.getCurrentUserId());
|
||||||
|
mapper.insert(vo);
|
||||||
|
return vo.getRequestId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 승인 처리 — approval_status = APPROVED, approver_id = 현재 사용자, approved_at = now.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void approve(Long requestId) {
|
||||||
|
ChargebackWaiverRequestResp resp = mapper.selectById(requestId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
if (!"REQUESTED".equals(resp.getApprovalStatus())) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
mapper.updateApproval(requestId, "APPROVED", SecurityUtil.getCurrentUserId(), LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 반려 처리 — approval_status = REJECTED.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void reject(Long requestId) {
|
||||||
|
ChargebackWaiverRequestResp resp = mapper.selectById(requestId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
if (!"REQUESTED".equals(resp.getApprovalStatus())) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
mapper.updateApproval(requestId, "REJECTED", SecurityUtil.getCurrentUserId(), LocalDateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package com.ga.api.service.commission;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.common.util.DateUtil;
|
||||||
|
import com.ga.core.mapper.commission.ClawbackInstallmentPlanMapper;
|
||||||
|
import com.ga.core.mapper.commission.ClawbackReceivableMapper;
|
||||||
|
import com.ga.core.vo.commission.*;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class ClawbackInstallmentService {
|
||||||
|
|
||||||
|
private final ClawbackInstallmentPlanMapper planMapper;
|
||||||
|
private final ClawbackReceivableMapper receivableMapper;
|
||||||
|
|
||||||
|
public PageResponse<ClawbackInstallmentPlanResp> list(ClawbackInstallmentPlanSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(planMapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClawbackInstallmentPlanResp detail(Long planId) {
|
||||||
|
ClawbackInstallmentPlanResp resp = planMapper.selectById(planId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ClawbackInstallmentPlanVO> listByReceivable(Long receivableId) {
|
||||||
|
return planMapper.selectByReceivable(receivableId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 환수채권을 N회차로 균등 분할상환 계획 생성.
|
||||||
|
* 마지막 회차는 잔차(총액 - 앞 회차 합계) 흡수.
|
||||||
|
* 기준월: baseMonth(YYYYMM), due_month = baseMonth + (회차-1)
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void generate(Long receivableId, int count, String baseMonth) {
|
||||||
|
if (count < 1) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
|
||||||
|
// 이미 SCHEDULED 계획이 있으면 중복 방지
|
||||||
|
List<ClawbackInstallmentPlanVO> existing = planMapper.selectByReceivable(receivableId);
|
||||||
|
boolean hasScheduled = existing.stream()
|
||||||
|
.anyMatch(p -> "SCHEDULED".equals(p.getStatus()));
|
||||||
|
if (hasScheduled) throw new BizException(ErrorCode.DUPLICATE_DATA);
|
||||||
|
|
||||||
|
ClawbackReceivableResp receivableResp = receivableMapper.selectById(receivableId);
|
||||||
|
if (receivableResp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
|
||||||
|
BigDecimal total = receivableResp.getBalance();
|
||||||
|
if (total == null || total.compareTo(BigDecimal.ZERO) <= 0)
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
|
||||||
|
BigDecimal unit = total.divide(BigDecimal.valueOf(count), 2, RoundingMode.DOWN);
|
||||||
|
BigDecimal sumBefore = unit.multiply(BigDecimal.valueOf(count - 1));
|
||||||
|
BigDecimal last = total.subtract(sumBefore);
|
||||||
|
|
||||||
|
List<ClawbackInstallmentPlanVO> plans = new ArrayList<>();
|
||||||
|
for (int i = 1; i <= count; i++) {
|
||||||
|
ClawbackInstallmentPlanVO vo = new ClawbackInstallmentPlanVO();
|
||||||
|
vo.setReceivableId(receivableId);
|
||||||
|
vo.setInstallmentNo(i);
|
||||||
|
vo.setInstallmentAmount(i == count ? last : unit);
|
||||||
|
vo.setDueMonth(DateUtil.addMonth(baseMonth, i - 1));
|
||||||
|
vo.setStatus("SCHEDULED");
|
||||||
|
plans.add(vo);
|
||||||
|
}
|
||||||
|
planMapper.insertBatch(plans);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 분할상환 계획 취소 (SCHEDULED → CANCELLED).
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void cancel(Long planId) {
|
||||||
|
ClawbackInstallmentPlanResp resp = planMapper.selectById(planId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
if (!"SCHEDULED".equals(resp.getStatus())) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
planMapper.updateStatus(planId, "CANCELLED");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package com.ga.api.service.commission;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.mapper.commission.LapsedReinstatementRecoveryMapper;
|
||||||
|
import com.ga.core.vo.commission.*;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class LapsedReinstatementService {
|
||||||
|
|
||||||
|
private final LapsedReinstatementRecoveryMapper mapper;
|
||||||
|
|
||||||
|
public PageResponse<LapsedReinstatementRecoveryResp> list(LapsedReinstatementRecoverySearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(mapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
public LapsedReinstatementRecoveryResp detail(Long recoveryId) {
|
||||||
|
LapsedReinstatementRecoveryResp resp = mapper.selectById(recoveryId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 부활 등록 — withheld_amount 입력, status = PENDING.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Long create(LapsedReinstatementRecoverySaveReq req) {
|
||||||
|
LapsedReinstatementRecoveryVO vo = new LapsedReinstatementRecoveryVO();
|
||||||
|
vo.setContractId(req.getContractId());
|
||||||
|
vo.setAgentId(req.getAgentId());
|
||||||
|
vo.setLapsedMonth(req.getLapsedMonth());
|
||||||
|
vo.setReinstatedMonth(req.getReinstatedMonth());
|
||||||
|
vo.setWithheldAmount(req.getWithheldAmount());
|
||||||
|
vo.setRepaidAmount(java.math.BigDecimal.ZERO);
|
||||||
|
vo.setStatus("PENDING");
|
||||||
|
mapper.insert(vo);
|
||||||
|
return vo.getRecoveryId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 재지급 처리 — status = REPAID, repaid_amount = withheld_amount.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void repay(Long recoveryId) {
|
||||||
|
LapsedReinstatementRecoveryResp resp = mapper.selectById(recoveryId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
if (!"PENDING".equals(resp.getStatus())) throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
mapper.updateStatus(recoveryId, "REPAID", resp.getWithheldAmount());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 취소 — status = CANCELLED.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public void cancel(Long recoveryId) {
|
||||||
|
LapsedReinstatementRecoveryResp resp = mapper.selectById(recoveryId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
if ("REPAID".equals(resp.getStatus()) || "CANCELLED".equals(resp.getStatus())) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER);
|
||||||
|
}
|
||||||
|
mapper.updateStatus(recoveryId, "CANCELLED", java.math.BigDecimal.ZERO);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,9 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@@ -86,6 +89,24 @@ public class PersistencyBonusService {
|
|||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void upsertBonus(PersistencyBonusSaveReq req) {
|
public void upsertBonus(PersistencyBonusSaveReq req) {
|
||||||
|
BigDecimal bonusAmount = req.getBonusAmount();
|
||||||
|
|
||||||
|
// 지속수당 지급 게이트: 룰이 지정되면 유지율이 임계(retention_rate_threshold) 이상이어야 지급.
|
||||||
|
// FIXED형 룰이면 지급액을 룰의 bonus_value(정액)로 도출한다.
|
||||||
|
if (req.getRuleId() != null) {
|
||||||
|
PersistencyBonusRuleResp rule = ruleMapper.selectById(req.getRuleId());
|
||||||
|
if (rule == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
if (rule.getRetentionRateThreshold() != null
|
||||||
|
&& (req.getRetentionRate() == null
|
||||||
|
|| req.getRetentionRate().compareTo(rule.getRetentionRateThreshold()) < 0)) {
|
||||||
|
throw new BizException(ErrorCode.INVALID_PARAMETER,
|
||||||
|
"유지율이 지급 임계(" + rule.getRetentionRateThreshold() + ")에 미달합니다.");
|
||||||
|
}
|
||||||
|
if ("FIXED".equals(rule.getBonusType()) && rule.getBonusValue() != null) {
|
||||||
|
bonusAmount = rule.getBonusValue().setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PersistencyBonusVO vo = new PersistencyBonusVO();
|
PersistencyBonusVO vo = new PersistencyBonusVO();
|
||||||
vo.setAgentId(req.getAgentId());
|
vo.setAgentId(req.getAgentId());
|
||||||
vo.setRuleId(req.getRuleId());
|
vo.setRuleId(req.getRuleId());
|
||||||
@@ -93,7 +114,7 @@ public class PersistencyBonusService {
|
|||||||
vo.setRetentionMonth(req.getRetentionMonth());
|
vo.setRetentionMonth(req.getRetentionMonth());
|
||||||
vo.setSettleMonth(req.getSettleMonth());
|
vo.setSettleMonth(req.getSettleMonth());
|
||||||
vo.setRetentionRate(req.getRetentionRate());
|
vo.setRetentionRate(req.getRetentionRate());
|
||||||
vo.setBonusAmount(req.getBonusAmount());
|
vo.setBonusAmount(bonusAmount);
|
||||||
vo.setStatus("CALCULATED");
|
vo.setStatus("CALCULATED");
|
||||||
bonusMapper.upsert(vo);
|
bonusMapper.upsert(vo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package com.ga.api.service.commission;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.model.PageResponse;
|
||||||
|
import com.ga.core.mapper.commission.RecruiterCommissionLedgerMapper;
|
||||||
|
import com.ga.core.mapper.commission.RecruiterCommissionRuleMapper;
|
||||||
|
import com.ga.core.vo.commission.*;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.math.RoundingMode;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class RecruiterCommissionService {
|
||||||
|
|
||||||
|
private final RecruiterCommissionRuleMapper ruleMapper;
|
||||||
|
private final RecruiterCommissionLedgerMapper ledgerMapper;
|
||||||
|
|
||||||
|
// ── Rule ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public PageResponse<RecruiterCommissionRuleResp> listRules(RecruiterCommissionRuleSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(ruleMapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecruiterCommissionRuleResp detailRule(Long ruleId) {
|
||||||
|
RecruiterCommissionRuleResp resp = ruleMapper.selectById(ruleId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long createRule(RecruiterCommissionRuleSaveReq req) {
|
||||||
|
RecruiterCommissionRuleVO vo = new RecruiterCommissionRuleVO();
|
||||||
|
vo.setRecruiterType(req.getRecruiterType());
|
||||||
|
vo.setCalcType(req.getCalcType());
|
||||||
|
vo.setCommissionRate(req.getCommissionRate());
|
||||||
|
vo.setFixedAmount(req.getFixedAmount());
|
||||||
|
vo.setEffectiveFrom(req.getEffectiveFrom());
|
||||||
|
vo.setEffectiveTo(req.getEffectiveTo());
|
||||||
|
ruleMapper.insert(vo);
|
||||||
|
return vo.getRuleId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateRule(Long ruleId, RecruiterCommissionRuleSaveReq req) {
|
||||||
|
RecruiterCommissionRuleResp resp = ruleMapper.selectById(ruleId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
RecruiterCommissionRuleVO vo = new RecruiterCommissionRuleVO();
|
||||||
|
vo.setRuleId(ruleId);
|
||||||
|
vo.setRecruiterType(req.getRecruiterType());
|
||||||
|
vo.setCalcType(req.getCalcType());
|
||||||
|
vo.setCommissionRate(req.getCommissionRate());
|
||||||
|
vo.setFixedAmount(req.getFixedAmount());
|
||||||
|
vo.setEffectiveFrom(req.getEffectiveFrom());
|
||||||
|
vo.setEffectiveTo(req.getEffectiveTo());
|
||||||
|
ruleMapper.update(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteRule(Long ruleId) {
|
||||||
|
RecruiterCommissionRuleResp resp = ruleMapper.selectById(ruleId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
ruleMapper.delete(ruleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Ledger ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public PageResponse<RecruiterCommissionLedgerResp> listLedgers(RecruiterCommissionLedgerSearchParam param) {
|
||||||
|
param.startPage();
|
||||||
|
return PageResponse.of(ledgerMapper.selectList(param));
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecruiterCommissionLedgerResp detailLedger(Long ledgerId) {
|
||||||
|
RecruiterCommissionLedgerResp resp = ledgerMapper.selectById(ledgerId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도입수수료 원장 생성.
|
||||||
|
* 활성 룰 목록 중 recruiterType + 오늘 기준 유효한 룰 우선 적용:
|
||||||
|
* RATE → commissionAmount = baseAmount × commissionRate
|
||||||
|
* FIXED → commissionAmount = fixedAmount
|
||||||
|
* 룰 없음 → 요청 commissionAmount 사용 (수동 등록 호환).
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public Long createLedger(RecruiterCommissionLedgerSaveReq req) {
|
||||||
|
RecruiterCommissionRuleVO activeRule = findActiveRule(req.getRecruiterType());
|
||||||
|
|
||||||
|
BigDecimal commissionAmount;
|
||||||
|
if (activeRule != null) {
|
||||||
|
if ("FIXED".equals(activeRule.getCalcType())) {
|
||||||
|
if (activeRule.getFixedAmount() == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
commissionAmount = activeRule.getFixedAmount().setScale(2, RoundingMode.HALF_UP);
|
||||||
|
} else { // RATE
|
||||||
|
BigDecimal rate = activeRule.getCommissionRate();
|
||||||
|
if (rate == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
commissionAmount = req.getBaseAmount().multiply(rate).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commissionAmount = req.getCommissionAmount();
|
||||||
|
if (commissionAmount == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
RecruiterCommissionLedgerVO vo = new RecruiterCommissionLedgerVO();
|
||||||
|
vo.setRecruiterAgentId(req.getRecruiterAgentId());
|
||||||
|
vo.setRecruitAgentId(req.getRecruitAgentId());
|
||||||
|
vo.setSettleMonth(req.getSettleMonth());
|
||||||
|
vo.setRecruiterType(req.getRecruiterType());
|
||||||
|
vo.setBaseAmount(req.getBaseAmount());
|
||||||
|
vo.setCommissionAmount(commissionAmount);
|
||||||
|
vo.setStatus("CALCULATED");
|
||||||
|
ledgerMapper.insert(vo);
|
||||||
|
return vo.getLedgerId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** recruiterType + 오늘 날짜 기준 유효 룰 첫 번째 반환. */
|
||||||
|
private RecruiterCommissionRuleVO findActiveRule(String recruiterType) {
|
||||||
|
RecruiterCommissionRuleSearchParam param = new RecruiterCommissionRuleSearchParam();
|
||||||
|
param.setRecruiterType(recruiterType);
|
||||||
|
List<RecruiterCommissionRuleResp> list = ruleMapper.selectList(param);
|
||||||
|
java.time.LocalDate today = java.time.LocalDate.now();
|
||||||
|
return list.stream()
|
||||||
|
.filter(r -> !today.isBefore(r.getEffectiveFrom())
|
||||||
|
&& (r.getEffectiveTo() == null || !today.isAfter(r.getEffectiveTo())))
|
||||||
|
.findFirst()
|
||||||
|
.map(r -> {
|
||||||
|
RecruiterCommissionRuleVO vo = new RecruiterCommissionRuleVO();
|
||||||
|
vo.setRuleId(r.getRuleId());
|
||||||
|
vo.setRecruiterType(r.getRecruiterType());
|
||||||
|
vo.setCalcType(r.getCalcType());
|
||||||
|
vo.setCommissionRate(r.getCommissionRate());
|
||||||
|
vo.setFixedAmount(r.getFixedAmount());
|
||||||
|
vo.setEffectiveFrom(r.getEffectiveFrom());
|
||||||
|
vo.setEffectiveTo(r.getEffectiveTo());
|
||||||
|
return vo;
|
||||||
|
})
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.ga.api.service.commission;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.common.exception.ErrorCode;
|
||||||
|
import com.ga.common.util.DateUtil;
|
||||||
|
import com.ga.core.mapper.commission.ClawbackReceivableMapper;
|
||||||
|
import com.ga.core.mapper.commission.StatuteConfigMapper;
|
||||||
|
import com.ga.core.vo.commission.StatuteConfigResp;
|
||||||
|
import com.ga.core.vo.commission.StatuteConfigSaveReq;
|
||||||
|
import com.ga.core.vo.commission.StatuteConfigVO;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class StatuteService {
|
||||||
|
|
||||||
|
private final StatuteConfigMapper configMapper;
|
||||||
|
private final ClawbackReceivableMapper receivableMapper;
|
||||||
|
|
||||||
|
public List<StatuteConfigResp> listConfigs() {
|
||||||
|
return configMapper.selectList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StatuteConfigResp detailConfig(Long configId) {
|
||||||
|
StatuteConfigResp resp = configMapper.selectById(configId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Long createConfig(StatuteConfigSaveReq req) {
|
||||||
|
StatuteConfigVO vo = new StatuteConfigVO();
|
||||||
|
vo.setEntityType(req.getEntityType());
|
||||||
|
vo.setExpirationMonths(req.getExpirationMonths());
|
||||||
|
vo.setIsActive(req.getIsActive() != null ? req.getIsActive() : "Y");
|
||||||
|
configMapper.insert(vo);
|
||||||
|
return vo.getConfigId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateConfig(Long configId, StatuteConfigSaveReq req) {
|
||||||
|
StatuteConfigResp resp = configMapper.selectById(configId);
|
||||||
|
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
StatuteConfigVO vo = new StatuteConfigVO();
|
||||||
|
vo.setConfigId(configId);
|
||||||
|
vo.setEntityType(req.getEntityType());
|
||||||
|
vo.setExpirationMonths(req.getExpirationMonths());
|
||||||
|
vo.setIsActive(req.getIsActive() != null ? req.getIsActive() : resp.getIsActive());
|
||||||
|
configMapper.update(vo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 만료 대상 채권 일괄 대손 처리.
|
||||||
|
* beforeMonth = baseMonth - expiration_months (YYYYMM 연산).
|
||||||
|
* 처리 건수 반환.
|
||||||
|
*/
|
||||||
|
@Transactional
|
||||||
|
public int expireOverdue(String baseMonth) {
|
||||||
|
StatuteConfigResp config = configMapper.selectByEntityType("CLAWBACK_RECEIVABLE");
|
||||||
|
if (config == null || !"Y".equals(config.getIsActive())) {
|
||||||
|
throw new BizException(ErrorCode.NOT_FOUND);
|
||||||
|
}
|
||||||
|
String beforeMonth = DateUtil.addMonth(baseMonth, -config.getExpirationMonths());
|
||||||
|
return receivableMapper.writeOffExpired(beforeMonth, "EXPIRED");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.ga.api.service.commission;
|
||||||
|
|
||||||
|
import com.ga.common.exception.BizException;
|
||||||
|
import com.ga.core.mapper.commission.PersistencyBonusMapper;
|
||||||
|
import com.ga.core.mapper.commission.PersistencyBonusRuleMapper;
|
||||||
|
import com.ga.core.vo.commission.PersistencyBonusRuleResp;
|
||||||
|
import com.ga.core.vo.commission.PersistencyBonusSaveReq;
|
||||||
|
import com.ga.core.vo.commission.PersistencyBonusVO;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class PersistencyBonusServiceTest {
|
||||||
|
|
||||||
|
@Mock PersistencyBonusRuleMapper ruleMapper;
|
||||||
|
@Mock PersistencyBonusMapper bonusMapper;
|
||||||
|
|
||||||
|
@InjectMocks PersistencyBonusService service;
|
||||||
|
|
||||||
|
private PersistencyBonusRuleResp rule(String bonusType, String threshold, String bonusValue) {
|
||||||
|
PersistencyBonusRuleResp r = new PersistencyBonusRuleResp();
|
||||||
|
r.setRuleId(7L);
|
||||||
|
r.setBonusType(bonusType);
|
||||||
|
r.setRetentionRateThreshold(threshold != null ? new BigDecimal(threshold) : null);
|
||||||
|
r.setBonusValue(bonusValue != null ? new BigDecimal(bonusValue) : null);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
private PersistencyBonusSaveReq req(String retentionRate, String bonusAmount) {
|
||||||
|
PersistencyBonusSaveReq req = new PersistencyBonusSaveReq();
|
||||||
|
req.setAgentId(1L);
|
||||||
|
req.setRuleId(7L);
|
||||||
|
req.setBaseMonth("202506");
|
||||||
|
req.setRetentionMonth(13);
|
||||||
|
req.setSettleMonth("202506");
|
||||||
|
req.setRetentionRate(retentionRate != null ? new BigDecimal(retentionRate) : null);
|
||||||
|
req.setBonusAmount(bonusAmount != null ? new BigDecimal(bonusAmount) : null);
|
||||||
|
return req;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("유지율이 임계 미달이면 지급 차단(BizException), 원장 미생성")
|
||||||
|
void belowThreshold_rejected() {
|
||||||
|
when(ruleMapper.selectById(7L)).thenReturn(rule("RATE", "0.90", null));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.upsertBonus(req("0.85", "30000")))
|
||||||
|
.isInstanceOf(BizException.class);
|
||||||
|
verify(bonusMapper, never()).upsert(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("유지율이 임계 이상이면 정상 지급(경계=이상)")
|
||||||
|
void atThreshold_ok() {
|
||||||
|
when(ruleMapper.selectById(7L)).thenReturn(rule("RATE", "0.90", null));
|
||||||
|
|
||||||
|
ArgumentCaptor<PersistencyBonusVO> captor = ArgumentCaptor.forClass(PersistencyBonusVO.class);
|
||||||
|
service.upsertBonus(req("0.90", "30000"));
|
||||||
|
|
||||||
|
verify(bonusMapper).upsert(captor.capture());
|
||||||
|
assertThat(captor.getValue().getBonusAmount()).isEqualByComparingTo("30000");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("FIXED형 룰이면 지급액을 룰의 bonus_value(정액)로 도출")
|
||||||
|
void fixedRule_derivesAmountFromRule() {
|
||||||
|
when(ruleMapper.selectById(7L)).thenReturn(rule("FIXED", "0.90", "50000"));
|
||||||
|
|
||||||
|
ArgumentCaptor<PersistencyBonusVO> captor = ArgumentCaptor.forClass(PersistencyBonusVO.class);
|
||||||
|
// 요청 bonusAmount(999)는 무시되고 룰 정액 50000 으로 대체
|
||||||
|
service.upsertBonus(req("0.95", "999"));
|
||||||
|
|
||||||
|
verify(bonusMapper).upsert(captor.capture());
|
||||||
|
assertThat(captor.getValue().getBonusAmount()).isEqualByComparingTo("50000.00");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,7 +50,7 @@ public class CommissionCalculator {
|
|||||||
if (agent == null) return null;
|
if (agent == null) return null;
|
||||||
LocalDate baseDate = c.getContractDate() != null ? c.getContractDate() : LocalDate.now();
|
LocalDate baseDate = c.getContractDate() != null ? c.getContractDate() : LocalDate.now();
|
||||||
|
|
||||||
BigDecimal companyRate = ruleMapper.findCompanyRate(c.getProductId(), year, baseDate);
|
BigDecimal companyRate = ruleMapper.findCompanyRate(c.getProductId(), year, baseDate, c.getSalesChannel());
|
||||||
if (companyRate == null) {
|
if (companyRate == null) {
|
||||||
log.warn("[수수료율 누락] company_rate 없음 → 0 폴백(과소지급 위험): contractId={}, productId={}, year={}, baseDate={}",
|
log.warn("[수수료율 누락] company_rate 없음 → 0 폴백(과소지급 위험): contractId={}, productId={}, year={}, baseDate={}",
|
||||||
c.getContractId(), c.getProductId(), year, baseDate);
|
c.getContractId(), c.getProductId(), year, baseDate);
|
||||||
@@ -58,7 +58,7 @@ public class CommissionCalculator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
BigDecimal payoutRate = ruleMapper.findPayoutRate(
|
BigDecimal payoutRate = ruleMapper.findPayoutRate(
|
||||||
agent.getGradeId(), c.getInsuranceType(), year, baseDate);
|
agent.getGradeId(), c.getInsuranceType(), year, baseDate, c.getSalesChannel());
|
||||||
if (payoutRate == null) {
|
if (payoutRate == null) {
|
||||||
log.warn("[수수료율 누락] payout_rate 없음 → 0 폴백(과소지급 위험): contractId={}, gradeId={}, insuranceType={}, year={}, baseDate={}",
|
log.warn("[수수료율 누락] payout_rate 없음 → 0 폴백(과소지급 위험): contractId={}, gradeId={}, insuranceType={}, year={}, baseDate={}",
|
||||||
c.getContractId(), agent.getGradeId(), c.getInsuranceType(), year, baseDate);
|
c.getContractId(), agent.getGradeId(), c.getInsuranceType(), year, baseDate);
|
||||||
|
|||||||
+7
-2
@@ -40,6 +40,9 @@ public class BatchInstallmentPlanGenerator {
|
|||||||
List<InstallmentPlanVO> plans = new ArrayList<>();
|
List<InstallmentPlanVO> plans = new ArrayList<>();
|
||||||
for (int i = 0; i < ratios.size(); i++) {
|
for (int i = 0; i < ratios.size(); i++) {
|
||||||
InstallmentRatioVO ratio = ratios.get(i);
|
InstallmentRatioVO ratio = ratios.get(i);
|
||||||
|
// 마스터의 plan_month 가 NULL 이면 연 단위(첫 달 지급), NOT NULL 이면 그 회차(월) 그대로 사용.
|
||||||
|
int planMonth = ratio.getPlanMonth() != null ? ratio.getPlanMonth() : 1;
|
||||||
|
|
||||||
BigDecimal amount;
|
BigDecimal amount;
|
||||||
if (fullSchedule && i == ratios.size() - 1) {
|
if (fullSchedule && i == ratios.size() - 1) {
|
||||||
amount = totalCommission.subtract(allocated);
|
amount = totalCommission.subtract(allocated);
|
||||||
@@ -52,8 +55,10 @@ public class BatchInstallmentPlanGenerator {
|
|||||||
InstallmentPlanVO plan = new InstallmentPlanVO();
|
InstallmentPlanVO plan = new InstallmentPlanVO();
|
||||||
plan.setContractId(contractId);
|
plan.setContractId(contractId);
|
||||||
plan.setPlanYear(ratio.getPlanYear());
|
plan.setPlanYear(ratio.getPlanYear());
|
||||||
plan.setPlanMonth(1);
|
plan.setPlanMonth(planMonth);
|
||||||
plan.setSettleMonth(contractDate.plusYears(ratio.getPlanYear()).format(MONTH_FMT));
|
plan.setSettleMonth(
|
||||||
|
contractDate.plusYears(ratio.getPlanYear()).plusMonths(planMonth - 1L).format(MONTH_FMT)
|
||||||
|
);
|
||||||
plan.setExpectedAmount(amount);
|
plan.setExpectedAmount(amount);
|
||||||
plan.setStatus(InstallmentStatus.SCHEDULED.code());
|
plan.setStatus(InstallmentStatus.SCHEDULED.code());
|
||||||
plans.add(plan);
|
plans.add(plan);
|
||||||
|
|||||||
@@ -4,13 +4,17 @@ import com.ga.batch.settlement.SettlementContext;
|
|||||||
import com.ga.batch.settlement.domain.BatchTaxCalculator;
|
import com.ga.batch.settlement.domain.BatchTaxCalculator;
|
||||||
import com.ga.batch.settlement.domain.TaxBreakdown;
|
import com.ga.batch.settlement.domain.TaxBreakdown;
|
||||||
import com.ga.common.util.MoneyUtil;
|
import com.ga.common.util.MoneyUtil;
|
||||||
|
import com.ga.core.mapper.commission.AdvancePaymentMapper;
|
||||||
|
import com.ga.core.mapper.commission.AdvanceRecoveryLedgerMapper;
|
||||||
import com.ga.core.mapper.commission.AwardLedgerMapper;
|
import com.ga.core.mapper.commission.AwardLedgerMapper;
|
||||||
import com.ga.core.mapper.commission.BranchMgrAllowanceLedgerMapper;
|
import com.ga.core.mapper.commission.BranchMgrAllowanceLedgerMapper;
|
||||||
import com.ga.core.mapper.commission.ClawbackReceivableMapper;
|
import com.ga.core.mapper.commission.ClawbackReceivableMapper;
|
||||||
import com.ga.core.mapper.commission.ClawbackRecoveryMapper;
|
import com.ga.core.mapper.commission.ClawbackRecoveryMapper;
|
||||||
import com.ga.core.mapper.commission.CollectionCommissionLedgerMapper;
|
import com.ga.core.mapper.commission.CollectionCommissionLedgerMapper;
|
||||||
|
import com.ga.core.mapper.commission.LapsedReinstatementRecoveryMapper;
|
||||||
import com.ga.core.mapper.commission.LifeSupportLedgerMapper;
|
import com.ga.core.mapper.commission.LifeSupportLedgerMapper;
|
||||||
import com.ga.core.mapper.commission.PersistencyBonusMapper;
|
import com.ga.core.mapper.commission.PersistencyBonusMapper;
|
||||||
|
import com.ga.core.mapper.commission.RecruiterCommissionLedgerMapper;
|
||||||
import com.ga.core.mapper.commission.ReferralCommissionLedgerMapper;
|
import com.ga.core.mapper.commission.ReferralCommissionLedgerMapper;
|
||||||
import com.ga.core.mapper.commission.ReinstatementCommissionLedgerMapper;
|
import com.ga.core.mapper.commission.ReinstatementCommissionLedgerMapper;
|
||||||
import com.ga.core.mapper.commission.RenewalCommissionLedgerMapper;
|
import com.ga.core.mapper.commission.RenewalCommissionLedgerMapper;
|
||||||
@@ -24,6 +28,8 @@ import com.ga.core.mapper.settle.ChargebackMapper;
|
|||||||
import com.ga.core.mapper.settle.CommissionStatementMapper;
|
import com.ga.core.mapper.settle.CommissionStatementMapper;
|
||||||
import com.ga.core.mapper.settle.OverrideSettleMapper;
|
import com.ga.core.mapper.settle.OverrideSettleMapper;
|
||||||
import com.ga.core.mapper.settle.SettleMasterMapper;
|
import com.ga.core.mapper.settle.SettleMasterMapper;
|
||||||
|
import com.ga.core.vo.commission.AdvancePaymentVO;
|
||||||
|
import com.ga.core.vo.commission.AdvanceRecoveryLedgerVO;
|
||||||
import com.ga.core.vo.commission.ClawbackReceivableResp;
|
import com.ga.core.vo.commission.ClawbackReceivableResp;
|
||||||
import com.ga.core.vo.commission.ClawbackReceivableVO;
|
import com.ga.core.vo.commission.ClawbackReceivableVO;
|
||||||
import com.ga.core.vo.commission.ClawbackRecoveryVO;
|
import com.ga.core.vo.commission.ClawbackRecoveryVO;
|
||||||
@@ -96,6 +102,14 @@ public class AggregateStep implements Tasklet {
|
|||||||
private final LifeSupportLedgerMapper lifeSupportMapper;
|
private final LifeSupportLedgerMapper lifeSupportMapper;
|
||||||
private final BranchMgrAllowanceLedgerMapper branchMgrAllowanceMapper;
|
private final BranchMgrAllowanceLedgerMapper branchMgrAllowanceMapper;
|
||||||
|
|
||||||
|
// MOD-1 도입수수료 / MOD-4 익월부활재지급
|
||||||
|
private final RecruiterCommissionLedgerMapper recruiterCommissionMapper;
|
||||||
|
private final LapsedReinstatementRecoveryMapper lapsedReinstatementMapper;
|
||||||
|
|
||||||
|
// MOD-2 선지급 상계
|
||||||
|
private final AdvancePaymentMapper advancePaymentMapper;
|
||||||
|
private final AdvanceRecoveryLedgerMapper advanceRecoveryMapper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
||||||
log.info("[Step8 aggregate] settleMonth={}", ctx.getSettleMonth());
|
log.info("[Step8 aggregate] settleMonth={}", ctx.getSettleMonth());
|
||||||
@@ -125,6 +139,11 @@ public class AggregateStep implements Tasklet {
|
|||||||
Map<Long, BigDecimal> lifeSupportMap = toMap(lifeSupportMapper.aggregateByAgent(ctx.getSettleMonth()));
|
Map<Long, BigDecimal> lifeSupportMap = toMap(lifeSupportMapper.aggregateByAgent(ctx.getSettleMonth()));
|
||||||
Map<Long, BigDecimal> branchMgrMap = toMap(branchMgrAllowanceMapper.aggregateByAgent(ctx.getSettleMonth()));
|
Map<Long, BigDecimal> branchMgrMap = toMap(branchMgrAllowanceMapper.aggregateByAgent(ctx.getSettleMonth()));
|
||||||
|
|
||||||
|
// MOD-1 도입수수료 (recruiter_agent_id 기준 agentId)
|
||||||
|
Map<Long, BigDecimal> recruiterCommMap = toMap(recruiterCommissionMapper.aggregateByAgent(ctx.getSettleMonth()));
|
||||||
|
// MOD-4 익월부활재지급 (REPAID 상태만)
|
||||||
|
Map<Long, BigDecimal> lapsedReinstatMap = toMap(lapsedReinstatementMapper.aggregateByAgent(ctx.getSettleMonth()));
|
||||||
|
|
||||||
// 합집합 agent_id
|
// 합집합 agent_id
|
||||||
Set<Long> agentIds = new HashSet<>();
|
Set<Long> agentIds = new HashSet<>();
|
||||||
agentIds.addAll(recruitMap.keySet());
|
agentIds.addAll(recruitMap.keySet());
|
||||||
@@ -142,6 +161,8 @@ public class AggregateStep implements Tasklet {
|
|||||||
agentIds.addAll(awardMap.keySet());
|
agentIds.addAll(awardMap.keySet());
|
||||||
agentIds.addAll(lifeSupportMap.keySet());
|
agentIds.addAll(lifeSupportMap.keySet());
|
||||||
agentIds.addAll(branchMgrMap.keySet());
|
agentIds.addAll(branchMgrMap.keySet());
|
||||||
|
agentIds.addAll(recruiterCommMap.keySet());
|
||||||
|
agentIds.addAll(lapsedReinstatMap.keySet());
|
||||||
|
|
||||||
// 활성 설계사 누락 보충
|
// 활성 설계사 누락 보충
|
||||||
AgentSearchParam ap = new AgentSearchParam();
|
AgentSearchParam ap = new AgentSearchParam();
|
||||||
@@ -168,6 +189,9 @@ public class AggregateStep implements Tasklet {
|
|||||||
// 동일 월 재실행 시 중복 회수가 발생하지 않도록 보장한다.
|
// 동일 월 재실행 시 중복 회수가 발생하지 않도록 보장한다.
|
||||||
reverseClawbackRecovery(ctx.getSettleMonth());
|
reverseClawbackRecovery(ctx.getSettleMonth());
|
||||||
|
|
||||||
|
// MOD-2 멱등 reverse: 선지급 상계 이력 역산 후 advance_payment balance 복원
|
||||||
|
reverseAdvanceRecovery(ctx.getSettleMonth());
|
||||||
|
|
||||||
for (Long agentId : agentIds) {
|
for (Long agentId : agentIds) {
|
||||||
BigDecimal recruit = recruitMap.getOrDefault(agentId, BigDecimal.ZERO);
|
BigDecimal recruit = recruitMap.getOrDefault(agentId, BigDecimal.ZERO);
|
||||||
BigDecimal maintain = maintainMap.getOrDefault(agentId, BigDecimal.ZERO);
|
BigDecimal maintain = maintainMap.getOrDefault(agentId, BigDecimal.ZERO);
|
||||||
@@ -187,6 +211,8 @@ public class AggregateStep implements Tasklet {
|
|||||||
otherCommission = MoneyUtil.add(otherCommission, awardMap.getOrDefault(agentId, BigDecimal.ZERO));
|
otherCommission = MoneyUtil.add(otherCommission, awardMap.getOrDefault(agentId, BigDecimal.ZERO));
|
||||||
otherCommission = MoneyUtil.add(otherCommission, lifeSupportMap.getOrDefault(agentId, BigDecimal.ZERO));
|
otherCommission = MoneyUtil.add(otherCommission, lifeSupportMap.getOrDefault(agentId, BigDecimal.ZERO));
|
||||||
otherCommission = MoneyUtil.add(otherCommission, branchMgrMap.getOrDefault(agentId, BigDecimal.ZERO));
|
otherCommission = MoneyUtil.add(otherCommission, branchMgrMap.getOrDefault(agentId, BigDecimal.ZERO));
|
||||||
|
otherCommission = MoneyUtil.add(otherCommission, recruiterCommMap.getOrDefault(agentId, BigDecimal.ZERO));
|
||||||
|
otherCommission = MoneyUtil.add(otherCommission, lapsedReinstatMap.getOrDefault(agentId, BigDecimal.ZERO));
|
||||||
|
|
||||||
BigDecimal gross = MoneyUtil.add(MoneyUtil.add(MoneyUtil.add(recruit, maintain), ov), ep)
|
BigDecimal gross = MoneyUtil.add(MoneyUtil.add(MoneyUtil.add(recruit, maintain), ov), ep)
|
||||||
.subtract(cb).subtract(em);
|
.subtract(cb).subtract(em);
|
||||||
@@ -230,6 +256,17 @@ public class AggregateStep implements Tasklet {
|
|||||||
log.info("[Step8 clawback] agentId={}, net={}, priorBalance={}, recovered={}, carryover={}, payable={}",
|
log.info("[Step8 clawback] agentId={}, net={}, priorBalance={}, recovered={}, carryover={}, payable={}",
|
||||||
agentId, netAmount, priorBalance, recovered, carryover, payable);
|
agentId, netAmount, priorBalance, recovered, carryover, payable);
|
||||||
|
|
||||||
|
// MOD-2 선지급 FIFO 상계: 환수채권 상계 후 payable에서 추가 차감
|
||||||
|
BigDecimal advanceOutstanding = advancePaymentMapper.outstandingByAgent(agentId);
|
||||||
|
if (advanceOutstanding == null) advanceOutstanding = BigDecimal.ZERO;
|
||||||
|
if (advanceOutstanding.signum() > 0 && payable.signum() > 0) {
|
||||||
|
BigDecimal recoveredAdvance = payable.min(advanceOutstanding);
|
||||||
|
payable = payable.subtract(recoveredAdvance);
|
||||||
|
applyAdvanceRecovery(agentId, ctx.getSettleMonth(), recoveredAdvance);
|
||||||
|
log.info("[Step8 advance] agentId={}, advanceOutstanding={}, recoveredAdvance={}, payable={}",
|
||||||
|
agentId, advanceOutstanding, recoveredAdvance, payable);
|
||||||
|
}
|
||||||
|
|
||||||
// settle_master UPSERT
|
// settle_master UPSERT
|
||||||
SettleMasterVO vo = new SettleMasterVO();
|
SettleMasterVO vo = new SettleMasterVO();
|
||||||
vo.setAgentId(agentId);
|
vo.setAgentId(agentId);
|
||||||
@@ -312,6 +349,74 @@ public class AggregateStep implements Tasklet {
|
|||||||
return RepeatStatus.FINISHED;
|
return RepeatStatus.FINISHED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MOD-2 멱등 reverse: settleMonth에 기록된 advance_recovery_ledger를 조회하여
|
||||||
|
* 각 advance_payment의 balance/recoveredAmount/status를 복원한 뒤 이력을 삭제한다.
|
||||||
|
* P9 reverseClawbackRecovery와 동일 사상.
|
||||||
|
*/
|
||||||
|
private void reverseAdvanceRecovery(String settleMonth) {
|
||||||
|
// 당월 상계 이력이 있는 선지급금만 월 단위로 역산(P9 reverseClawbackRecovery와 동일 사상).
|
||||||
|
for (Long advanceId : advanceRecoveryMapper.selectAdvanceIdsBySettleMonth(settleMonth)) {
|
||||||
|
List<com.ga.core.vo.commission.AdvanceRecoveryLedgerVO> prevLedgers =
|
||||||
|
advanceRecoveryMapper.selectBySettleMonth(advanceId, settleMonth);
|
||||||
|
if (prevLedgers.isEmpty()) continue;
|
||||||
|
|
||||||
|
BigDecimal toRestore = prevLedgers.stream()
|
||||||
|
.map(com.ga.core.vo.commission.AdvanceRecoveryLedgerVO::getRecoveryAmount)
|
||||||
|
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||||
|
|
||||||
|
com.ga.core.vo.commission.AdvancePaymentResp ap = advancePaymentMapper.selectById(advanceId);
|
||||||
|
if (ap == null) continue;
|
||||||
|
|
||||||
|
BigDecimal newRecovered = ap.getRecoveredAmount().subtract(toRestore);
|
||||||
|
BigDecimal newBalance = ap.getBalance().add(toRestore);
|
||||||
|
String newStatus = newRecovered.signum() == 0 ? "OUTSTANDING" : "PARTIAL";
|
||||||
|
|
||||||
|
AdvancePaymentVO updated = new AdvancePaymentVO();
|
||||||
|
updated.setAdvanceId(advanceId);
|
||||||
|
updated.setRecoveredAmount(newRecovered);
|
||||||
|
updated.setBalance(newBalance);
|
||||||
|
updated.setStatus(newStatus);
|
||||||
|
updated.setMemo(ap.getMemo());
|
||||||
|
advancePaymentMapper.update(updated);
|
||||||
|
|
||||||
|
advanceRecoveryMapper.deleteBySettleMonth(advanceId, settleMonth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MOD-2 FIFO 선지급 상계 적용: 오래된 advance_month 순으로 balance를 차감하고
|
||||||
|
* advance_recovery_ledger 이력을 INSERT한다.
|
||||||
|
*/
|
||||||
|
private void applyAdvanceRecovery(Long agentId, String settleMonth, BigDecimal recoveredTotal) {
|
||||||
|
List<AdvancePaymentVO> fifo = advancePaymentMapper.selectOutstandingFifo(agentId);
|
||||||
|
BigDecimal remaining = recoveredTotal;
|
||||||
|
for (AdvancePaymentVO ap : fifo) {
|
||||||
|
if (remaining.signum() == 0) break;
|
||||||
|
|
||||||
|
BigDecimal take = remaining.min(ap.getBalance());
|
||||||
|
BigDecimal newRecovered = ap.getRecoveredAmount().add(take);
|
||||||
|
BigDecimal newBalance = ap.getBalance().subtract(take);
|
||||||
|
String newStatus = newBalance.signum() == 0 ? "CLEARED" : "PARTIAL";
|
||||||
|
|
||||||
|
AdvancePaymentVO updated = new AdvancePaymentVO();
|
||||||
|
updated.setAdvanceId(ap.getAdvanceId());
|
||||||
|
updated.setRecoveredAmount(newRecovered);
|
||||||
|
updated.setBalance(newBalance);
|
||||||
|
updated.setStatus(newStatus);
|
||||||
|
updated.setMemo(ap.getMemo());
|
||||||
|
advancePaymentMapper.update(updated);
|
||||||
|
|
||||||
|
AdvanceRecoveryLedgerVO ledger = new AdvanceRecoveryLedgerVO();
|
||||||
|
ledger.setAdvanceId(ap.getAdvanceId());
|
||||||
|
ledger.setSettleMonth(settleMonth);
|
||||||
|
ledger.setRecoveryAmount(take);
|
||||||
|
advanceRecoveryMapper.insert(ledger);
|
||||||
|
|
||||||
|
remaining = remaining.subtract(take);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 멱등 reverse: settleMonth의 기존 clawback_recovery 이력을 조회하여
|
* 멱등 reverse: settleMonth의 기존 clawback_recovery 이력을 조회하여
|
||||||
* 각 receivable의 balance/recoveredAmount/status를 복원한 뒤 회수 이력을 전량 삭제한다.
|
* 각 receivable의 balance/recoveredAmount/status를 복원한 뒤 회수 이력을 전량 삭제한다.
|
||||||
|
|||||||
@@ -75,32 +75,43 @@ public class CalcRecruitStep implements Tasklet {
|
|||||||
LedgerVO led = calculator.calcRecruitLedger(c, ctx.getSettleMonth());
|
LedgerVO led = calculator.calcRecruitLedger(c, ctx.getSettleMonth());
|
||||||
if (led == null || MoneyUtil.isZero(led.getAgentAmount())) continue;
|
if (led == null || MoneyUtil.isZero(led.getAgentAmount())) continue;
|
||||||
|
|
||||||
// 1200%룰 검증 (1차년도 한도)
|
// 1200%룰 검증: 1차년 한도(FIRST_YEAR) + 누적 한도(TOTAL 1200%) 중
|
||||||
|
// 더 제한적인 한도로 당월 지급액을 clip, 초과분은 분급(7년)으로 이연.
|
||||||
if (c.getContractDate() != null) {
|
if (c.getContractDate() != null) {
|
||||||
|
BigDecimal originalAmount = led.getAgentAmount();
|
||||||
|
BigDecimal allowed = originalAmount;
|
||||||
|
|
||||||
ViolationResult firstYearCheck = regulatoryChecker.checkFirstYear(
|
ViolationResult firstYearCheck = regulatoryChecker.checkFirstYear(
|
||||||
c.getContractId(), led.getAgentAmount(), c.getContractDate());
|
c.getContractId(), originalAmount, c.getContractDate());
|
||||||
|
|
||||||
if (firstYearCheck.violated()) {
|
if (firstYearCheck.violated()) {
|
||||||
violationCount++;
|
BigDecimal fyAllowed = MoneyUtil.sub(
|
||||||
|
|
||||||
// 1차년 한도 잔여분까지만 당월 지급, 초과분은 분급(7년)으로 이연.
|
|
||||||
BigDecimal allowed = MoneyUtil.sub(
|
|
||||||
firstYearCheck.limitAmount(), firstYearCheck.currentTotal());
|
firstYearCheck.limitAmount(), firstYearCheck.currentTotal());
|
||||||
if (allowed.signum() < 0) allowed = BigDecimal.ZERO;
|
if (fyAllowed.signum() < 0) fyAllowed = BigDecimal.ZERO;
|
||||||
BigDecimal excess = MoneyUtil.sub(led.getAgentAmount(), allowed);
|
allowed = allowed.min(fyAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
if (excess.signum() > 0) {
|
ViolationResult totalCheck = regulatoryChecker.checkContractTotal(
|
||||||
// 초과분만 분급 계획 생성 (7년 35/25/15/10/7/5/3)
|
c.getContractId(), originalAmount, c.getContractDate());
|
||||||
List<InstallmentPlanVO> plans = installmentGenerator.generate(
|
if (totalCheck.violated()) {
|
||||||
c.getContractId(), excess,
|
BigDecimal totAllowed = MoneyUtil.sub(
|
||||||
c.getContractDate(), c.getInsuranceType());
|
totalCheck.limitAmount(), totalCheck.currentTotal());
|
||||||
installmentBatch.addAll(plans);
|
if (totAllowed.signum() < 0) totAllowed = BigDecimal.ZERO;
|
||||||
// 당월 원장은 한도까지만 지급(초과분 차감) + 세액 재계산
|
allowed = allowed.min(totAllowed);
|
||||||
led.setAgentAmount(allowed);
|
}
|
||||||
led.setTaxAmount(MoneyUtil.tax(allowed));
|
|
||||||
log.warn("[Step4] 1200%룰 1차년 한도초과 → clip+이연: contractId={}, 지급(한도잔여)={}, 이연(분급)={}",
|
BigDecimal excess = MoneyUtil.sub(originalAmount, allowed);
|
||||||
c.getContractId(), allowed, excess);
|
if (excess.signum() > 0) {
|
||||||
}
|
violationCount++;
|
||||||
|
// 초과분만 분급 계획 생성 (7년 35/25/15/10/7/5/3)
|
||||||
|
List<InstallmentPlanVO> plans = installmentGenerator.generate(
|
||||||
|
c.getContractId(), excess,
|
||||||
|
c.getContractDate(), c.getInsuranceType());
|
||||||
|
installmentBatch.addAll(plans);
|
||||||
|
// 당월 원장은 한도까지만 지급(초과분 차감) + 세액 재계산
|
||||||
|
led.setAgentAmount(allowed);
|
||||||
|
led.setTaxAmount(MoneyUtil.tax(allowed));
|
||||||
|
log.warn("[Step4] 1200%룰 한도초과 → clip+이연: contractId={}, 지급(한도잔여)={}, 이연(분급)={}, firstYear={}, total={}",
|
||||||
|
c.getContractId(), allowed, excess, firstYearCheck.violated(), totalCheck.violated());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ class CommissionCalculatorTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("calcRecruitLedger: premium × company_rate × payout_rate, 세금은 3.3%")
|
@DisplayName("calcRecruitLedger: premium × company_rate × payout_rate, 세금은 3.3%")
|
||||||
void recruit_basic() {
|
void recruit_basic() {
|
||||||
when(ruleMapper.findCompanyRate(eq(1L), eq(1), any(LocalDate.class)))
|
when(ruleMapper.findCompanyRate(eq(1L), eq(1), any(LocalDate.class), any()))
|
||||||
.thenReturn(new BigDecimal("7.5"));
|
.thenReturn(new BigDecimal("7.5"));
|
||||||
when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(1), any(LocalDate.class)))
|
when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(1), any(LocalDate.class), any()))
|
||||||
.thenReturn(new BigDecimal("60.0"));
|
.thenReturn(new BigDecimal("60.0"));
|
||||||
|
|
||||||
LedgerVO led = calculator.calcRecruitLedger(contract, "202405");
|
LedgerVO led = calculator.calcRecruitLedger(contract, "202405");
|
||||||
@@ -83,12 +83,30 @@ class CommissionCalculatorTest {
|
|||||||
assertThat(led.getPayMethod()).isEqualTo("MONTHLY");
|
assertThat(led.getPayMethod()).isEqualTo("MONTHLY");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("MOD-8 채널요율: 계약 sales_channel 이 요율 조회로 전달된다 (채널별 룰 선택)")
|
||||||
|
void recruit_passesSalesChannel() {
|
||||||
|
contract.setSalesChannel("BANCASSURANCE");
|
||||||
|
// 채널 일치 요율(방카)만 stub — 채널이 전달되지 않으면 매칭 실패로 0 폴백됨
|
||||||
|
when(ruleMapper.findCompanyRate(eq(1L), eq(1), any(LocalDate.class), eq("BANCASSURANCE")))
|
||||||
|
.thenReturn(new BigDecimal("5.0"));
|
||||||
|
when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(1), any(LocalDate.class), eq("BANCASSURANCE")))
|
||||||
|
.thenReturn(new BigDecimal("80.0"));
|
||||||
|
|
||||||
|
LedgerVO led = calculator.calcRecruitLedger(contract, "202405");
|
||||||
|
|
||||||
|
assertThat(led.getCompanyRate()).isEqualByComparingTo("5.0");
|
||||||
|
assertThat(led.getPayoutRate()).isEqualByComparingTo("80.0");
|
||||||
|
// 1,000,000 × 5% = 50,000 → × 80% = 40,000 (채널 요율이 실제로 적용됨)
|
||||||
|
assertThat(led.getAgentAmount()).isEqualByComparingTo("40000.00");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("calcMaintainLedger: 회차에 따라 commission_year 가 다름")
|
@DisplayName("calcMaintainLedger: 회차에 따라 commission_year 가 다름")
|
||||||
void maintain_year3() {
|
void maintain_year3() {
|
||||||
when(ruleMapper.findCompanyRate(eq(1L), eq(3), any(LocalDate.class)))
|
when(ruleMapper.findCompanyRate(eq(1L), eq(3), any(LocalDate.class), any()))
|
||||||
.thenReturn(new BigDecimal("3.0"));
|
.thenReturn(new BigDecimal("3.0"));
|
||||||
when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(3), any(LocalDate.class)))
|
when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(3), any(LocalDate.class), any()))
|
||||||
.thenReturn(new BigDecimal("50.0"));
|
.thenReturn(new BigDecimal("50.0"));
|
||||||
|
|
||||||
LedgerVO led = calculator.calcMaintainLedger(contract, "202405", 3);
|
LedgerVO led = calculator.calcMaintainLedger(contract, "202405", 3);
|
||||||
@@ -115,8 +133,8 @@ class CommissionCalculatorTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("규정 누락 시 0 으로 폴백 → 모든 금액 0")
|
@DisplayName("규정 누락 시 0 으로 폴백 → 모든 금액 0")
|
||||||
void missingRates_fallbackZero() {
|
void missingRates_fallbackZero() {
|
||||||
when(ruleMapper.findCompanyRate(any(), any(), any())).thenReturn(null);
|
when(ruleMapper.findCompanyRate(any(), any(), any(), any())).thenReturn(null);
|
||||||
when(ruleMapper.findPayoutRate(any(), any(), any(), any())).thenReturn(null);
|
when(ruleMapper.findPayoutRate(any(), any(), any(), any(), any())).thenReturn(null);
|
||||||
|
|
||||||
LedgerVO led = calculator.calcRecruitLedger(contract, "202405");
|
LedgerVO led = calculator.calcRecruitLedger(contract, "202405");
|
||||||
|
|
||||||
@@ -132,9 +150,9 @@ class CommissionCalculatorTest {
|
|||||||
@DisplayName("contractDate 가 null 이면 오늘 날짜로 대체 (예외 없음)")
|
@DisplayName("contractDate 가 null 이면 오늘 날짜로 대체 (예외 없음)")
|
||||||
void nullContractDate_usesToday() {
|
void nullContractDate_usesToday() {
|
||||||
contract.setContractDate(null);
|
contract.setContractDate(null);
|
||||||
when(ruleMapper.findCompanyRate(eq(1L), eq(1), any(LocalDate.class)))
|
when(ruleMapper.findCompanyRate(eq(1L), eq(1), any(LocalDate.class), any()))
|
||||||
.thenReturn(new BigDecimal("5"));
|
.thenReturn(new BigDecimal("5"));
|
||||||
when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(1), any(LocalDate.class)))
|
when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(1), any(LocalDate.class), any()))
|
||||||
.thenReturn(new BigDecimal("100"));
|
.thenReturn(new BigDecimal("100"));
|
||||||
|
|
||||||
LedgerVO led = calculator.calcRecruitLedger(contract, "202405");
|
LedgerVO led = calculator.calcRecruitLedger(contract, "202405");
|
||||||
@@ -148,8 +166,8 @@ class CommissionCalculatorTest {
|
|||||||
@DisplayName("premium 이 null 이면 0 으로 처리 (HALF_UP 0.00)")
|
@DisplayName("premium 이 null 이면 0 으로 처리 (HALF_UP 0.00)")
|
||||||
void nullPremium() {
|
void nullPremium() {
|
||||||
contract.setPremium(null);
|
contract.setPremium(null);
|
||||||
when(ruleMapper.findCompanyRate(any(), any(), any())).thenReturn(new BigDecimal("7.5"));
|
when(ruleMapper.findCompanyRate(any(), any(), any(), any())).thenReturn(new BigDecimal("7.5"));
|
||||||
when(ruleMapper.findPayoutRate(any(), any(), any(), any())).thenReturn(new BigDecimal("60"));
|
when(ruleMapper.findPayoutRate(any(), any(), any(), any(), any())).thenReturn(new BigDecimal("60"));
|
||||||
|
|
||||||
LedgerVO led = calculator.calcRecruitLedger(contract, "202405");
|
LedgerVO led = calculator.calcRecruitLedger(contract, "202405");
|
||||||
|
|
||||||
|
|||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package com.ga.batch.settlement.domain;
|
||||||
|
|
||||||
|
import com.ga.core.mapper.installment.InstallmentRatioMapper;
|
||||||
|
import com.ga.core.vo.installment.InstallmentPlanVO;
|
||||||
|
import com.ga.core.vo.installment.InstallmentRatioVO;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 분급 계획 생성기 — plan_month 반영 검증 (API InstallmentPlanGenerator와 동일 출력).
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class BatchInstallmentPlanGeneratorTest {
|
||||||
|
|
||||||
|
@Mock InstallmentRatioMapper ratioMapper;
|
||||||
|
@InjectMocks BatchInstallmentPlanGenerator generator;
|
||||||
|
|
||||||
|
static final LocalDate CONTRACT_DATE = LocalDate.of(2024, 1, 15);
|
||||||
|
|
||||||
|
private InstallmentRatioVO ratio(int year, Integer month, String pct) {
|
||||||
|
InstallmentRatioVO r = new InstallmentRatioVO();
|
||||||
|
r.setPlanYear(year);
|
||||||
|
r.setPlanMonth(month);
|
||||||
|
r.setRatioPercent(new BigDecimal(pct));
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("plan_month 가 설정되면 settleMonth = 계약일 +N년 +(plan_month-1)월")
|
||||||
|
void usesPlanMonthInSettleMonth() {
|
||||||
|
// 1차년 4월차 100% (단일 회차)
|
||||||
|
when(ratioMapper.selectAllActiveRatios(anyString(), anyString()))
|
||||||
|
.thenReturn(List.of(ratio(1, 4, "100")));
|
||||||
|
|
||||||
|
List<InstallmentPlanVO> plans = generator.generate(
|
||||||
|
100L, new BigDecimal("700000"), CONTRACT_DATE, "WHOLE_LIFE");
|
||||||
|
|
||||||
|
assertThat(plans).hasSize(1);
|
||||||
|
InstallmentPlanVO p = plans.get(0);
|
||||||
|
assertThat(p.getPlanMonth()).isEqualTo(4);
|
||||||
|
// 2024-01 +1년 +3개월 = 2025-04
|
||||||
|
assertThat(p.getSettleMonth()).isEqualTo("202504");
|
||||||
|
assertThat(p.getExpectedAmount()).isEqualByComparingTo("700000");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("plan_month 가 NULL 이면 연 단위(첫 달) — settleMonth = 계약일 +N년")
|
||||||
|
void nullPlanMonthDefaultsToYearStart() {
|
||||||
|
when(ratioMapper.selectAllActiveRatios(any(), anyString()))
|
||||||
|
.thenReturn(List.of(ratio(2, null, "100")));
|
||||||
|
|
||||||
|
List<InstallmentPlanVO> plans = generator.generate(
|
||||||
|
100L, new BigDecimal("500000"), CONTRACT_DATE, null);
|
||||||
|
|
||||||
|
InstallmentPlanVO p = plans.get(0);
|
||||||
|
assertThat(p.getPlanMonth()).isEqualTo(1);
|
||||||
|
// 2024-01 +2년 +0개월 = 2026-01
|
||||||
|
assertThat(p.getSettleMonth()).isEqualTo("202601");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -129,9 +129,9 @@ class SettlementScenarioTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("① 보험 인입 → 모집수수료 원장: 1,000,000 ×60% ×75% = 450,000, 세금 14,850")
|
@DisplayName("① 보험 인입 → 모집수수료 원장: 1,000,000 ×60% ×75% = 450,000, 세금 14,850")
|
||||||
void s1_보험인입_모집수수료_원장계산() {
|
void s1_보험인입_모집수수료_원장계산() {
|
||||||
when(ruleMapper.findCompanyRate(eq(PRODUCT_ID), eq(1), any(LocalDate.class)))
|
when(ruleMapper.findCompanyRate(eq(PRODUCT_ID), eq(1), any(LocalDate.class), any()))
|
||||||
.thenReturn(new BigDecimal("60.0"));
|
.thenReturn(new BigDecimal("60.0"));
|
||||||
when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(1), any(LocalDate.class)))
|
when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(1), any(LocalDate.class), any()))
|
||||||
.thenReturn(new BigDecimal("75.0"));
|
.thenReturn(new BigDecimal("75.0"));
|
||||||
|
|
||||||
LedgerVO led = commissionCalculator.calcRecruitLedger(contract, "202401");
|
LedgerVO led = commissionCalculator.calcRecruitLedger(contract, "202401");
|
||||||
@@ -226,9 +226,9 @@ class SettlementScenarioTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("④ 유지수수료 2차년: 1,000,000 ×10% ×50% = 50,000")
|
@DisplayName("④ 유지수수료 2차년: 1,000,000 ×10% ×50% = 50,000")
|
||||||
void s4_유지수수료_2차년() {
|
void s4_유지수수료_2차년() {
|
||||||
when(ruleMapper.findCompanyRate(eq(PRODUCT_ID), eq(2), any(LocalDate.class)))
|
when(ruleMapper.findCompanyRate(eq(PRODUCT_ID), eq(2), any(LocalDate.class), any()))
|
||||||
.thenReturn(new BigDecimal("10.0"));
|
.thenReturn(new BigDecimal("10.0"));
|
||||||
when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(2), any(LocalDate.class)))
|
when(ruleMapper.findPayoutRate(eq(5L), eq("WHOLE_LIFE"), eq(2), any(LocalDate.class), any()))
|
||||||
.thenReturn(new BigDecimal("50.0"));
|
.thenReturn(new BigDecimal("50.0"));
|
||||||
|
|
||||||
LedgerVO led = commissionCalculator.calcMaintainLedger(contract, "202501", 2);
|
LedgerVO led = commissionCalculator.calcMaintainLedger(contract, "202501", 2);
|
||||||
@@ -241,10 +241,10 @@ class SettlementScenarioTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("⑤ 계약 승계: 유지수수료는 후임(박승계 등급3 40%)에게 귀속, 모집은 원설계사 유지")
|
@DisplayName("⑤ 계약 승계: 유지수수료는 후임(박승계 등급3 40%)에게 귀속, 모집은 원설계사 유지")
|
||||||
void s5_계약승계_유지수수료_후임귀속() {
|
void s5_계약승계_유지수수료_후임귀속() {
|
||||||
when(ruleMapper.findCompanyRate(eq(PRODUCT_ID), eq(2), any(LocalDate.class)))
|
when(ruleMapper.findCompanyRate(eq(PRODUCT_ID), eq(2), any(LocalDate.class), any()))
|
||||||
.thenReturn(new BigDecimal("10.0"));
|
.thenReturn(new BigDecimal("10.0"));
|
||||||
// 후임 박승계 등급3 의 2차년 지급률 40%
|
// 후임 박승계 등급3 의 2차년 지급률 40%
|
||||||
when(ruleMapper.findPayoutRate(eq(3L), eq("WHOLE_LIFE"), eq(2), any(LocalDate.class)))
|
when(ruleMapper.findPayoutRate(eq(3L), eq("WHOLE_LIFE"), eq(2), any(LocalDate.class), any()))
|
||||||
.thenReturn(new BigDecimal("40.0"));
|
.thenReturn(new BigDecimal("40.0"));
|
||||||
|
|
||||||
LedgerVO led = commissionCalculator.calcMaintainLedger(contract, "202501", 2, PARK);
|
LedgerVO led = commissionCalculator.calcMaintainLedger(contract, "202501", 2, PARK);
|
||||||
|
|||||||
@@ -57,6 +57,30 @@ class CalcRecruitStepTest {
|
|||||||
step = new CalcRecruitStep(ctx, contractMapper, recruitMapper,
|
step = new CalcRecruitStep(ctx, contractMapper, recruitMapper,
|
||||||
calculator, regulatoryChecker, installmentGenerator, installmentPlanMapper);
|
calculator, regulatoryChecker, installmentGenerator, installmentPlanMapper);
|
||||||
when(ctx.getSettleMonth()).thenReturn(MONTH);
|
when(ctx.getSettleMonth()).thenReturn(MONTH);
|
||||||
|
// 기본: 두 한도 모두 위반 아님(각 테스트에서 필요한 한도만 violated 로 오버라이드)
|
||||||
|
when(regulatoryChecker.checkFirstYear(any(), any(BigDecimal.class), any(LocalDate.class)))
|
||||||
|
.thenReturn(ViolationResult.ok(BigDecimal.ZERO, null, "FIRST_YEAR_RATIO"));
|
||||||
|
when(regulatoryChecker.checkContractTotal(any(), any(BigDecimal.class), any(LocalDate.class)))
|
||||||
|
.thenReturn(ViolationResult.ok(BigDecimal.ZERO, null, "TOTAL_RATIO"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContractResp contract() {
|
||||||
|
ContractResp c = new ContractResp();
|
||||||
|
c.setContractId(CONTRACT_ID);
|
||||||
|
c.setAgentId(10L);
|
||||||
|
c.setInsuranceType("WHOLE_LIFE");
|
||||||
|
c.setContractDate(CONTRACT_DATE);
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LedgerVO recruitLedger(ContractResp c, String amount, String tax) {
|
||||||
|
LedgerVO led = new LedgerVO();
|
||||||
|
led.setContractId(CONTRACT_ID);
|
||||||
|
led.setAgentId(10L);
|
||||||
|
led.setAgentAmount(new BigDecimal(amount));
|
||||||
|
led.setTaxAmount(new BigDecimal(tax));
|
||||||
|
when(calculator.calcRecruitLedger(eq(c), eq(MONTH))).thenReturn(led);
|
||||||
|
return led;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -100,4 +124,62 @@ class CalcRecruitStepTest {
|
|||||||
assertThat(saved.getAgentAmount()).isEqualByComparingTo("350000");
|
assertThat(saved.getAgentAmount()).isEqualByComparingTo("350000");
|
||||||
assertThat(saved.getTaxAmount()).isEqualByComparingTo("11550.00");
|
assertThat(saved.getTaxAmount()).isEqualByComparingTo("11550.00");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("1200% 누적한도 잔여 300,000 < 모집수수료 450,000 → 300,000 지급 + 초과 150,000 분급 이연")
|
||||||
|
void contractTotalLimit_clipAndDefer() {
|
||||||
|
ContractResp c = contract();
|
||||||
|
when(contractMapper.selectList(any())).thenReturn(List.of(c));
|
||||||
|
recruitLedger(c, "450000", "14850");
|
||||||
|
|
||||||
|
// 1차년은 통과(기본), 누적 1200% 한도만 위반: 한도 12,000,000 / 기지급 11,700,000 → 잔여 300,000
|
||||||
|
when(regulatoryChecker.checkContractTotal(eq(CONTRACT_ID), any(BigDecimal.class), any(LocalDate.class)))
|
||||||
|
.thenReturn(ViolationResult.violated(
|
||||||
|
new BigDecimal("11700000"), new BigDecimal("12000000"), "TOTAL_RATIO", "초과"));
|
||||||
|
when(installmentGenerator.generate(eq(CONTRACT_ID), any(BigDecimal.class), any(LocalDate.class), anyString()))
|
||||||
|
.thenReturn(List.of(new InstallmentPlanVO()));
|
||||||
|
ArgumentCaptor<List<LedgerVO>> ledgerCaptor = ArgumentCaptor.forClass(List.class);
|
||||||
|
when(recruitMapper.batchInsert(ledgerCaptor.capture())).thenReturn(1);
|
||||||
|
|
||||||
|
step.execute(null, null);
|
||||||
|
|
||||||
|
ArgumentCaptor<BigDecimal> excessCaptor = ArgumentCaptor.forClass(BigDecimal.class);
|
||||||
|
org.mockito.Mockito.verify(installmentGenerator)
|
||||||
|
.generate(eq(CONTRACT_ID), excessCaptor.capture(), any(LocalDate.class), anyString());
|
||||||
|
assertThat(excessCaptor.getValue()).isEqualByComparingTo("150000");
|
||||||
|
|
||||||
|
LedgerVO saved = ledgerCaptor.getValue().get(0);
|
||||||
|
assertThat(saved.getAgentAmount()).isEqualByComparingTo("300000");
|
||||||
|
assertThat(saved.getTaxAmount()).isEqualByComparingTo("9900.00");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("1차년·누적 둘 다 위반 → 더 제한적인 누적한도 잔여 300,000 으로 clip")
|
||||||
|
void bothLimits_mostRestrictiveApplies() {
|
||||||
|
ContractResp c = contract();
|
||||||
|
when(contractMapper.selectList(any())).thenReturn(List.of(c));
|
||||||
|
recruitLedger(c, "450000", "14850");
|
||||||
|
|
||||||
|
// 1차년 잔여 350,000, 누적 잔여 300,000 → min(450000, 350000, 300000) = 300,000
|
||||||
|
when(regulatoryChecker.checkFirstYear(eq(CONTRACT_ID), any(BigDecimal.class), any(LocalDate.class)))
|
||||||
|
.thenReturn(ViolationResult.violated(
|
||||||
|
BigDecimal.ZERO, new BigDecimal("350000"), "FIRST_YEAR_RATIO", "초과"));
|
||||||
|
when(regulatoryChecker.checkContractTotal(eq(CONTRACT_ID), any(BigDecimal.class), any(LocalDate.class)))
|
||||||
|
.thenReturn(ViolationResult.violated(
|
||||||
|
new BigDecimal("11700000"), new BigDecimal("12000000"), "TOTAL_RATIO", "초과"));
|
||||||
|
when(installmentGenerator.generate(eq(CONTRACT_ID), any(BigDecimal.class), any(LocalDate.class), anyString()))
|
||||||
|
.thenReturn(List.of(new InstallmentPlanVO()));
|
||||||
|
ArgumentCaptor<List<LedgerVO>> ledgerCaptor = ArgumentCaptor.forClass(List.class);
|
||||||
|
when(recruitMapper.batchInsert(ledgerCaptor.capture())).thenReturn(1);
|
||||||
|
|
||||||
|
step.execute(null, null);
|
||||||
|
|
||||||
|
ArgumentCaptor<BigDecimal> excessCaptor = ArgumentCaptor.forClass(BigDecimal.class);
|
||||||
|
org.mockito.Mockito.verify(installmentGenerator)
|
||||||
|
.generate(eq(CONTRACT_ID), excessCaptor.capture(), any(LocalDate.class), anyString());
|
||||||
|
assertThat(excessCaptor.getValue()).isEqualByComparingTo("150000");
|
||||||
|
|
||||||
|
LedgerVO saved = ledgerCaptor.getValue().get(0);
|
||||||
|
assertThat(saved.getAgentAmount()).isEqualByComparingTo("300000");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
-- V116: MOD-1 슈퍼바이저/도입수수료 (recruiter/development commission)
|
||||||
|
-- agent.recruiter_agent_id 컬럼 추가
|
||||||
|
-- recruiter_commission_rule / recruiter_commission_ledger 테이블
|
||||||
|
-- 메뉴 RECRUITER_COMM (GRP_COMMISSION 하위)
|
||||||
|
-- 공통코드 RECRUITER_TYPE
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. agent 테이블에 도입자 컬럼 추가
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE agent ADD COLUMN IF NOT EXISTS recruiter_agent_id BIGINT REFERENCES agent(agent_id);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN agent.recruiter_agent_id IS '도입자 설계사 ID (NULL=도입자 없음)';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. recruiter_commission_rule (도입수수료 룰 마스터)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE recruiter_commission_rule (
|
||||||
|
rule_id BIGSERIAL PRIMARY KEY,
|
||||||
|
recruiter_type VARCHAR(20) NOT NULL,
|
||||||
|
calc_type VARCHAR(10) NOT NULL,
|
||||||
|
commission_rate NUMERIC(7,4),
|
||||||
|
fixed_amount NUMERIC(15,2),
|
||||||
|
effective_from DATE NOT NULL,
|
||||||
|
effective_to DATE,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT chk_rcr_recruiter_type CHECK (recruiter_type IN ('NEW_RECRUIT','DEVELOPMENT','TRAINING')),
|
||||||
|
CONSTRAINT chk_rcr_calc_type CHECK (calc_type IN ('RATE','FIXED')),
|
||||||
|
CONSTRAINT chk_rcr_rate_range CHECK (commission_rate IS NULL OR (commission_rate >= 0 AND commission_rate <= 1))
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE recruiter_commission_rule IS '도입수수료 룰 마스터';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_rule.rule_id IS 'PK';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_rule.recruiter_type IS '도입유형: NEW_RECRUIT/DEVELOPMENT/TRAINING';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_rule.calc_type IS '계산방식: RATE(정률)/FIXED(정액)';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_rule.commission_rate IS '정률 요율 (0~1)';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_rule.fixed_amount IS '정액 금액';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_rule.effective_from IS '적용 시작일';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_rule.effective_to IS '적용 종료일 (NULL=무기한)';
|
||||||
|
|
||||||
|
CREATE INDEX idx_rcr_type_from ON recruiter_commission_rule(recruiter_type, effective_from);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. recruiter_commission_ledger (도입수수료 원장)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE recruiter_commission_ledger (
|
||||||
|
ledger_id BIGSERIAL PRIMARY KEY,
|
||||||
|
recruiter_agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
|
||||||
|
recruit_agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
|
||||||
|
settle_month CHAR(6) NOT NULL,
|
||||||
|
recruiter_type VARCHAR(20) NOT NULL,
|
||||||
|
base_amount NUMERIC(15,2) NOT NULL,
|
||||||
|
commission_amount NUMERIC(15,2) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT chk_rcl_status CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED'))
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE recruiter_commission_ledger IS '도입수수료 원장';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_ledger.ledger_id IS 'PK';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_ledger.recruiter_agent_id IS '도입자 설계사 ID';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_ledger.recruit_agent_id IS '피도입(신규) 설계사 ID';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_ledger.settle_month IS '정산월 YYYYMM';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_ledger.recruiter_type IS '도입유형';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_ledger.base_amount IS '기준금액 (피도입 당월 모집수수료 합)';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_ledger.commission_amount IS '도입수수료 금액';
|
||||||
|
COMMENT ON COLUMN recruiter_commission_ledger.status IS '상태: CALCULATED/CONFIRMED/PAID/CANCELLED';
|
||||||
|
|
||||||
|
CREATE INDEX idx_rcl_recruiter_month ON recruiter_commission_ledger(recruiter_agent_id, settle_month);
|
||||||
|
CREATE INDEX idx_rcl_recruit_month ON recruiter_commission_ledger(recruit_agent_id, settle_month);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 메뉴 INSERT: RECRUITER_COMM (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
|
||||||
|
('RECRUITER_COMM', '도입수수료', '/commission/recruiter', 'commission/RecruiterCommission', 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/CREATE/UPDATE/DELETE/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),
|
||||||
|
('CREATE', '등록', 20),
|
||||||
|
('UPDATE', '수정', 30),
|
||||||
|
('DELETE', '삭제', 40),
|
||||||
|
('EXPORT', '엑셀', 60)
|
||||||
|
) AS p(code, name, sort)
|
||||||
|
WHERE m.menu_code = 'RECRUITER_COMM'
|
||||||
|
ON CONFLICT (menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 6. 역할별 권한 부여
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- SUPER_ADMIN / ADMIN — 전체 권한
|
||||||
|
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 = 'RECRUITER_COMM'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- MANAGER — READ/CREATE/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 = 'MANAGER'
|
||||||
|
AND mp.perm_code IN ('READ','CREATE','UPDATE','EXPORT')
|
||||||
|
AND m.menu_code = 'RECRUITER_COMM'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- AGENT — 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 = 'AGENT'
|
||||||
|
AND mp.perm_code = 'READ'
|
||||||
|
AND m.menu_code = 'RECRUITER_COMM'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 7. 공통코드: RECRUITER_TYPE
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('RECRUITER_TYPE', '도입수수료유형', 'NEW_RECRUIT=신인도입/DEVELOPMENT=육성/TRAINING=교육', 'Y', 460)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, sort_order) VALUES
|
||||||
|
('RECRUITER_TYPE', 'NEW_RECRUIT', '신인도입', 10),
|
||||||
|
('RECRUITER_TYPE', 'DEVELOPMENT', '육성', 20),
|
||||||
|
('RECRUITER_TYPE', 'TRAINING', '교육', 30)
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
-- V117: MOD-2 선지급/차익정산 (advance payment recovery + delta settlement)
|
||||||
|
-- advance_payment / advance_recovery_ledger 테이블
|
||||||
|
-- 메뉴 ADVANCE_PAYMENT (GRP_SETTLE 하위)
|
||||||
|
-- 공통코드 ADVANCE_STATUS
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. advance_payment (선지급금 마스터)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE advance_payment (
|
||||||
|
advance_id BIGSERIAL PRIMARY KEY,
|
||||||
|
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
|
||||||
|
advance_month CHAR(6) NOT NULL,
|
||||||
|
principal_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',
|
||||||
|
memo VARCHAR(200),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT chk_adv_principal_pos CHECK (principal_amount > 0),
|
||||||
|
CONSTRAINT chk_adv_balance_nn CHECK (balance >= 0),
|
||||||
|
CONSTRAINT chk_adv_status CHECK (status IN ('OUTSTANDING','PARTIAL','CLEARED','WAIVED'))
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE advance_payment IS '선지급금 마스터';
|
||||||
|
COMMENT ON COLUMN advance_payment.advance_id IS 'PK';
|
||||||
|
COMMENT ON COLUMN advance_payment.agent_id IS '설계사 ID';
|
||||||
|
COMMENT ON COLUMN advance_payment.advance_month IS '선지급 발생월 YYYYMM';
|
||||||
|
COMMENT ON COLUMN advance_payment.principal_amount IS '원금';
|
||||||
|
COMMENT ON COLUMN advance_payment.recovered_amount IS '회수 누계';
|
||||||
|
COMMENT ON COLUMN advance_payment.balance IS '잔액 (principal - recovered)';
|
||||||
|
COMMENT ON COLUMN advance_payment.status IS '상태: OUTSTANDING/PARTIAL/CLEARED/WAIVED';
|
||||||
|
COMMENT ON COLUMN advance_payment.memo IS '메모';
|
||||||
|
|
||||||
|
CREATE INDEX idx_adv_agent_status ON advance_payment(agent_id, status);
|
||||||
|
CREATE INDEX idx_adv_month ON advance_payment(advance_month);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. advance_recovery_ledger (선지급 상계 이력)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE advance_recovery_ledger (
|
||||||
|
recovery_id BIGSERIAL PRIMARY KEY,
|
||||||
|
advance_id BIGINT NOT NULL REFERENCES advance_payment(advance_id),
|
||||||
|
settle_month CHAR(6) NOT NULL,
|
||||||
|
recovery_amount NUMERIC(15,2) NOT NULL,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
CONSTRAINT chk_arl_recovery_pos CHECK (recovery_amount > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE advance_recovery_ledger IS '선지급 상계 이력';
|
||||||
|
COMMENT ON COLUMN advance_recovery_ledger.recovery_id IS 'PK';
|
||||||
|
COMMENT ON COLUMN advance_recovery_ledger.advance_id IS '선지급금 ID';
|
||||||
|
COMMENT ON COLUMN advance_recovery_ledger.settle_month IS '상계 정산월 YYYYMM';
|
||||||
|
COMMENT ON COLUMN advance_recovery_ledger.recovery_amount IS '당월 상계 금액';
|
||||||
|
|
||||||
|
CREATE INDEX idx_arl_advance ON advance_recovery_ledger(advance_id);
|
||||||
|
CREATE INDEX idx_arl_settle_month ON advance_recovery_ledger(settle_month);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 메뉴 INSERT: ADVANCE_PAYMENT (GRP_SETTLE 하위 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
|
||||||
|
('ADVANCE_PAYMENT', '선지급정산', '/settle/advance-payment', 'settle/AdvancePayment', 130)
|
||||||
|
) AS x(code, name, path, cmpt, sort)
|
||||||
|
ON p.menu_code = 'GRP_SETTLE'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 메뉴 권한 항목 등록 (READ/CREATE/UPDATE/DELETE/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),
|
||||||
|
('CREATE', '등록', 20),
|
||||||
|
('UPDATE', '수정', 30),
|
||||||
|
('DELETE', '삭제', 40),
|
||||||
|
('EXPORT', '엑셀', 60)
|
||||||
|
) AS p(code, name, sort)
|
||||||
|
WHERE m.menu_code = 'ADVANCE_PAYMENT'
|
||||||
|
ON CONFLICT (menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. 역할별 권한 부여
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- SUPER_ADMIN / ADMIN — 전체 권한
|
||||||
|
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 = 'ADVANCE_PAYMENT'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- MANAGER — READ/CREATE/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 = 'MANAGER'
|
||||||
|
AND mp.perm_code IN ('READ','CREATE','UPDATE','EXPORT')
|
||||||
|
AND m.menu_code = 'ADVANCE_PAYMENT'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- AGENT — 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 = 'AGENT'
|
||||||
|
AND mp.perm_code = 'READ'
|
||||||
|
AND m.menu_code = 'ADVANCE_PAYMENT'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 6. 공통코드: ADVANCE_STATUS
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('ADVANCE_STATUS', '선지급상태', 'OUTSTANDING=미상환/PARTIAL=부분/CLEARED=완료/WAIVED=면제', 'Y', 470)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, sort_order) VALUES
|
||||||
|
('ADVANCE_STATUS', 'OUTSTANDING', '미상환', 10),
|
||||||
|
('ADVANCE_STATUS', 'PARTIAL', '부분상환', 20),
|
||||||
|
('ADVANCE_STATUS', 'CLEARED', '상환완료', 30),
|
||||||
|
('ADVANCE_STATUS', 'WAIVED', '면제', 40)
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
-- V118: MOD-3 환수 분할상환 (installment clawback)
|
||||||
|
-- clawback_installment_plan 테이블
|
||||||
|
-- 메뉴 CLAWBACK_INSTALLMENT (GRP_COMMISSION 하위)
|
||||||
|
-- 공통코드 CB_INSTALLMENT_STATUS
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. clawback_installment_plan (환수 분할상환 계획)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE clawback_installment_plan (
|
||||||
|
plan_id BIGSERIAL PRIMARY KEY,
|
||||||
|
receivable_id BIGINT NOT NULL REFERENCES clawback_receivable(receivable_id),
|
||||||
|
installment_no INT NOT NULL,
|
||||||
|
installment_amount NUMERIC(15,2) NOT NULL,
|
||||||
|
due_month CHAR(6) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'SCHEDULED',
|
||||||
|
recovered_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT chk_cip_amount_pos CHECK (installment_amount > 0),
|
||||||
|
CONSTRAINT chk_cip_status CHECK (status IN ('SCHEDULED','RECOVERED','CANCELLED')),
|
||||||
|
CONSTRAINT uq_cip_receivable_no UNIQUE (receivable_id, installment_no)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE clawback_installment_plan IS '환수 분할상환 계획';
|
||||||
|
COMMENT ON COLUMN clawback_installment_plan.plan_id IS 'PK';
|
||||||
|
COMMENT ON COLUMN clawback_installment_plan.receivable_id IS '환수채권 ID';
|
||||||
|
COMMENT ON COLUMN clawback_installment_plan.installment_no IS '분할 회차 번호 (1부터)';
|
||||||
|
COMMENT ON COLUMN clawback_installment_plan.installment_amount IS '회차 납부 금액';
|
||||||
|
COMMENT ON COLUMN clawback_installment_plan.due_month IS '납부 예정월 YYYYMM';
|
||||||
|
COMMENT ON COLUMN clawback_installment_plan.status IS '상태: SCHEDULED/RECOVERED/CANCELLED';
|
||||||
|
COMMENT ON COLUMN clawback_installment_plan.recovered_at IS '실제 회수 일시';
|
||||||
|
|
||||||
|
CREATE INDEX idx_cip_due_status ON clawback_installment_plan(due_month, status);
|
||||||
|
CREATE INDEX idx_cip_receivable_id ON clawback_installment_plan(receivable_id);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. 메뉴 INSERT: CLAWBACK_INSTALLMENT (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_INSTALLMENT', '환수분할상환', '/commission/clawback-installment', 'commission/ClawbackInstallment', 160)
|
||||||
|
) 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);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 메뉴 권한 항목 등록 (READ/CREATE/UPDATE/DELETE/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),
|
||||||
|
('CREATE', '등록', 20),
|
||||||
|
('UPDATE', '수정', 30),
|
||||||
|
('DELETE', '삭제', 40),
|
||||||
|
('EXPORT', '엑셀', 60)
|
||||||
|
) AS p(code, name, sort)
|
||||||
|
WHERE m.menu_code = 'CLAWBACK_INSTALLMENT'
|
||||||
|
ON CONFLICT (menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 역할별 권한 부여
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- SUPER_ADMIN / ADMIN — 전체 권한
|
||||||
|
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_INSTALLMENT'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- MANAGER — READ/CREATE/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 = 'MANAGER'
|
||||||
|
AND mp.perm_code IN ('READ','CREATE','UPDATE','EXPORT')
|
||||||
|
AND m.menu_code = 'CLAWBACK_INSTALLMENT'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- AGENT — 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 = 'AGENT'
|
||||||
|
AND mp.perm_code = 'READ'
|
||||||
|
AND m.menu_code = 'CLAWBACK_INSTALLMENT'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. 공통코드: CB_INSTALLMENT_STATUS
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('CB_INSTALLMENT_STATUS', '환수분할상환상태', 'SCHEDULED=예정/RECOVERED=회수/CANCELLED=취소', 'Y', 480)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, sort_order) VALUES
|
||||||
|
('CB_INSTALLMENT_STATUS', 'SCHEDULED', '예정', 10),
|
||||||
|
('CB_INSTALLMENT_STATUS', 'RECOVERED', '회수', 20),
|
||||||
|
('CB_INSTALLMENT_STATUS', 'CANCELLED', '취소', 30)
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
-- V119: MOD-4 익월 부활 재지급 (lapsed reinstatement repayment)
|
||||||
|
-- lapsed_reinstatement_recovery 테이블
|
||||||
|
-- 메뉴 LAPSED_REINSTATEMENT (GRP_COMMISSION 하위)
|
||||||
|
-- 공통코드 LAPSE_RECOVERY_STATUS
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. lapsed_reinstatement_recovery (익월 부활 재지급 원장)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE lapsed_reinstatement_recovery (
|
||||||
|
recovery_id BIGSERIAL PRIMARY KEY,
|
||||||
|
contract_id BIGINT NOT NULL REFERENCES contract(contract_id),
|
||||||
|
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
|
||||||
|
lapsed_month CHAR(6) NOT NULL,
|
||||||
|
reinstated_month CHAR(6) NOT NULL,
|
||||||
|
withheld_amount NUMERIC(15,2) NOT NULL,
|
||||||
|
repaid_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT chk_lrr_withheld_nn CHECK (withheld_amount >= 0),
|
||||||
|
CONSTRAINT chk_lrr_status CHECK (status IN ('PENDING','REPAID','CANCELLED'))
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE lapsed_reinstatement_recovery IS '익월 부활 재지급 원장';
|
||||||
|
COMMENT ON COLUMN lapsed_reinstatement_recovery.recovery_id IS 'PK';
|
||||||
|
COMMENT ON COLUMN lapsed_reinstatement_recovery.contract_id IS '계약 ID';
|
||||||
|
COMMENT ON COLUMN lapsed_reinstatement_recovery.agent_id IS '설계사 ID';
|
||||||
|
COMMENT ON COLUMN lapsed_reinstatement_recovery.lapsed_month IS '실효 발생월 YYYYMM';
|
||||||
|
COMMENT ON COLUMN lapsed_reinstatement_recovery.reinstated_month IS '부활 확정월 YYYYMM';
|
||||||
|
COMMENT ON COLUMN lapsed_reinstatement_recovery.withheld_amount IS '보류된 수수료 금액';
|
||||||
|
COMMENT ON COLUMN lapsed_reinstatement_recovery.repaid_amount IS '재지급 완료 금액';
|
||||||
|
COMMENT ON COLUMN lapsed_reinstatement_recovery.status IS '상태: PENDING/REPAID/CANCELLED';
|
||||||
|
|
||||||
|
CREATE INDEX idx_lrr_agent_reinstated ON lapsed_reinstatement_recovery(agent_id, reinstated_month);
|
||||||
|
CREATE INDEX idx_lrr_contract ON lapsed_reinstatement_recovery(contract_id);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. 메뉴 INSERT: LAPSED_REINSTATEMENT (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
|
||||||
|
('LAPSED_REINSTATEMENT', '익월부활재지급', '/commission/lapsed-reinstatement', 'commission/LapsedReinstatement', 170)
|
||||||
|
) 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);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 메뉴 권한 항목 등록 (READ/CREATE/UPDATE/DELETE/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),
|
||||||
|
('CREATE', '등록', 20),
|
||||||
|
('UPDATE', '수정', 30),
|
||||||
|
('DELETE', '삭제', 40),
|
||||||
|
('EXPORT', '엑셀', 60)
|
||||||
|
) AS p(code, name, sort)
|
||||||
|
WHERE m.menu_code = 'LAPSED_REINSTATEMENT'
|
||||||
|
ON CONFLICT (menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 역할별 권한 부여
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- SUPER_ADMIN / ADMIN — 전체 권한
|
||||||
|
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 = 'LAPSED_REINSTATEMENT'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- MANAGER — READ/CREATE/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 = 'MANAGER'
|
||||||
|
AND mp.perm_code IN ('READ','CREATE','UPDATE','EXPORT')
|
||||||
|
AND m.menu_code = 'LAPSED_REINSTATEMENT'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- AGENT — 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 = 'AGENT'
|
||||||
|
AND mp.perm_code = 'READ'
|
||||||
|
AND m.menu_code = 'LAPSED_REINSTATEMENT'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. 공통코드: LAPSE_RECOVERY_STATUS
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('LAPSE_RECOVERY_STATUS', '부활재지급상태', 'PENDING=대기/REPAID=재지급완료/CANCELLED=취소', 'Y', 490)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, sort_order) VALUES
|
||||||
|
('LAPSE_RECOVERY_STATUS', 'PENDING', '대기', 10),
|
||||||
|
('LAPSE_RECOVERY_STATUS', 'REPAID', '재지급완료', 20),
|
||||||
|
('LAPSE_RECOVERY_STATUS', 'CANCELLED', '취소', 30)
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
-- V120: MOD-5 정산대사 입수 (carrier reconciliation IN)
|
||||||
|
-- carrier_reconciliation_in / reconciliation_variance 테이블
|
||||||
|
-- 메뉴 CARRIER_RECON_IN (GRP_SETTLE 하위)
|
||||||
|
-- 공통코드 RECON_IN_STATUS
|
||||||
|
-- insurance_company PK: company_id (SERIAL, V2 확인)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. carrier_reconciliation_in (보험사 정산대사 입수 마스터)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE carrier_reconciliation_in (
|
||||||
|
recon_id BIGSERIAL PRIMARY KEY,
|
||||||
|
company_id BIGINT NOT NULL REFERENCES insurance_company(company_id),
|
||||||
|
settle_month CHAR(6) NOT NULL,
|
||||||
|
file_name VARCHAR(200),
|
||||||
|
record_count INT NOT NULL DEFAULT 0,
|
||||||
|
reported_total NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
system_total NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
variance_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
|
||||||
|
status VARCHAR(20) NOT NULL DEFAULT 'RECEIVED',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT chk_cri_status CHECK (status IN ('RECEIVED','MATCHED','VARIANCE','APPROVED')),
|
||||||
|
CONSTRAINT uq_cri_company_month UNIQUE (company_id, settle_month)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE carrier_reconciliation_in IS '보험사 정산대사 입수 마스터';
|
||||||
|
COMMENT ON COLUMN carrier_reconciliation_in.recon_id IS 'PK';
|
||||||
|
COMMENT ON COLUMN carrier_reconciliation_in.company_id IS '보험사 ID';
|
||||||
|
COMMENT ON COLUMN carrier_reconciliation_in.settle_month IS '정산월 YYYYMM';
|
||||||
|
COMMENT ON COLUMN carrier_reconciliation_in.file_name IS '입수 파일명';
|
||||||
|
COMMENT ON COLUMN carrier_reconciliation_in.record_count IS '보험사 통보 건수';
|
||||||
|
COMMENT ON COLUMN carrier_reconciliation_in.reported_total IS '보험사 통보 금액 합계';
|
||||||
|
COMMENT ON COLUMN carrier_reconciliation_in.system_total IS 'GA 시스템 집계 금액';
|
||||||
|
COMMENT ON COLUMN carrier_reconciliation_in.variance_amount IS '차이 금액 (reported - system)';
|
||||||
|
COMMENT ON COLUMN carrier_reconciliation_in.status IS '상태: RECEIVED/MATCHED/VARIANCE/APPROVED';
|
||||||
|
|
||||||
|
CREATE INDEX idx_cri_settle_month ON carrier_reconciliation_in(settle_month);
|
||||||
|
CREATE INDEX idx_cri_company_month ON carrier_reconciliation_in(company_id, settle_month);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. reconciliation_variance (대사 차이 상세)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE reconciliation_variance (
|
||||||
|
variance_id BIGSERIAL PRIMARY KEY,
|
||||||
|
recon_id BIGINT NOT NULL REFERENCES carrier_reconciliation_in(recon_id),
|
||||||
|
policy_no VARCHAR(50),
|
||||||
|
expected_amount NUMERIC(15,2),
|
||||||
|
reported_amount NUMERIC(15,2),
|
||||||
|
variance_reason VARCHAR(100),
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE reconciliation_variance IS '대사 차이 상세';
|
||||||
|
COMMENT ON COLUMN reconciliation_variance.variance_id IS 'PK';
|
||||||
|
COMMENT ON COLUMN reconciliation_variance.recon_id IS '대사 입수 ID';
|
||||||
|
COMMENT ON COLUMN reconciliation_variance.policy_no IS '증권번호';
|
||||||
|
COMMENT ON COLUMN reconciliation_variance.expected_amount IS 'GA 시스템 예상 금액';
|
||||||
|
COMMENT ON COLUMN reconciliation_variance.reported_amount IS '보험사 통보 금액';
|
||||||
|
COMMENT ON COLUMN reconciliation_variance.variance_reason IS '차이 사유';
|
||||||
|
|
||||||
|
CREATE INDEX idx_rv_recon_id ON reconciliation_variance(recon_id);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 메뉴 INSERT: CARRIER_RECON_IN (GRP_SETTLE 하위 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
|
||||||
|
('CARRIER_RECON_IN', '정산대사입수', '/settle/carrier-recon-in', 'settle/CarrierReconIn', 140)
|
||||||
|
) AS x(code, name, path, cmpt, sort)
|
||||||
|
ON p.menu_code = 'GRP_SETTLE'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 메뉴 권한 항목 등록 (READ/CREATE/UPDATE/DELETE/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),
|
||||||
|
('CREATE', '등록', 20),
|
||||||
|
('UPDATE', '수정', 30),
|
||||||
|
('DELETE', '삭제', 40),
|
||||||
|
('EXPORT', '엑셀', 60)
|
||||||
|
) AS p(code, name, sort)
|
||||||
|
WHERE m.menu_code = 'CARRIER_RECON_IN'
|
||||||
|
ON CONFLICT (menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. 역할별 권한 부여
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- SUPER_ADMIN / ADMIN — 전체 권한
|
||||||
|
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 = 'CARRIER_RECON_IN'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- MANAGER — READ/CREATE/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 = 'MANAGER'
|
||||||
|
AND mp.perm_code IN ('READ','CREATE','UPDATE','EXPORT')
|
||||||
|
AND m.menu_code = 'CARRIER_RECON_IN'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- AGENT — 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 = 'AGENT'
|
||||||
|
AND mp.perm_code = 'READ'
|
||||||
|
AND m.menu_code = 'CARRIER_RECON_IN'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 6. 공통코드: RECON_IN_STATUS
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('RECON_IN_STATUS', '정산대사상태', 'RECEIVED=입수/MATCHED=일치/VARIANCE=차이/APPROVED=승인', 'Y', 500)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, sort_order) VALUES
|
||||||
|
('RECON_IN_STATUS', 'RECEIVED', '입수', 10),
|
||||||
|
('RECON_IN_STATUS', 'MATCHED', '일치', 20),
|
||||||
|
('RECON_IN_STATUS', 'VARIANCE', '차이', 30),
|
||||||
|
('RECON_IN_STATUS', 'APPROVED', '승인', 40)
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
-- V121: MOD-6 환수 시효/소멸 (statute of limitations)
|
||||||
|
-- statute_config 테이블 + 시드 1행
|
||||||
|
-- 메뉴 STATUTE_CONFIG (GRP_COMMISSION 하위)
|
||||||
|
-- clawback_receivable.status CHECK: WRITTEN_OFF 이미 존재 (V111 확인) → 제약 변경 불필요
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. statute_config (환수 시효 설정)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE statute_config (
|
||||||
|
config_id BIGSERIAL PRIMARY KEY,
|
||||||
|
entity_type VARCHAR(30) NOT NULL,
|
||||||
|
expiration_months INT NOT NULL,
|
||||||
|
is_active CHAR(1) NOT NULL DEFAULT 'Y',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT chk_sc_entity_type CHECK (entity_type IN ('CLAWBACK_RECEIVABLE')),
|
||||||
|
CONSTRAINT chk_sc_expiration_pos CHECK (expiration_months > 0),
|
||||||
|
CONSTRAINT chk_sc_is_active CHECK (is_active IN ('Y','N')),
|
||||||
|
CONSTRAINT uq_sc_entity_type UNIQUE (entity_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE statute_config IS '환수 시효 설정';
|
||||||
|
COMMENT ON COLUMN statute_config.config_id IS 'PK';
|
||||||
|
COMMENT ON COLUMN statute_config.entity_type IS '대상 엔티티 타입 (CLAWBACK_RECEIVABLE)';
|
||||||
|
COMMENT ON COLUMN statute_config.expiration_months IS '시효 만료 개월수';
|
||||||
|
COMMENT ON COLUMN statute_config.is_active IS '활성 여부 (Y/N)';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. 시드 데이터: CLAWBACK_RECEIVABLE 36개월
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO statute_config (entity_type, expiration_months, is_active)
|
||||||
|
VALUES ('CLAWBACK_RECEIVABLE', 36, 'Y')
|
||||||
|
ON CONFLICT (entity_type) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 메뉴 INSERT: STATUTE_CONFIG (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
|
||||||
|
('STATUTE_CONFIG', '환수시효관리', '/commission/statute', 'commission/StatuteConfig', 180)
|
||||||
|
) 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);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 메뉴 권한 항목 등록 (READ/CREATE/UPDATE/DELETE/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),
|
||||||
|
('CREATE', '등록', 20),
|
||||||
|
('UPDATE', '수정', 30),
|
||||||
|
('DELETE', '삭제', 40),
|
||||||
|
('EXPORT', '엑셀', 60)
|
||||||
|
) AS p(code, name, sort)
|
||||||
|
WHERE m.menu_code = 'STATUTE_CONFIG'
|
||||||
|
ON CONFLICT (menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. 역할별 권한 부여
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- SUPER_ADMIN / ADMIN — 전체 권한
|
||||||
|
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 = 'STATUTE_CONFIG'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- MANAGER — READ/CREATE/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 = 'MANAGER'
|
||||||
|
AND mp.perm_code IN ('READ','CREATE','UPDATE','EXPORT')
|
||||||
|
AND m.menu_code = 'STATUTE_CONFIG'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- AGENT — 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 = 'AGENT'
|
||||||
|
AND mp.perm_code = 'READ'
|
||||||
|
AND m.menu_code = 'STATUTE_CONFIG'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
-- V122: MOD-7 환수 감면/면제 (chargeback waiver/reduction)
|
||||||
|
-- chargeback_waiver_request 테이블
|
||||||
|
-- 메뉴 CHARGEBACK_WAIVER (GRP_COMMISSION 하위)
|
||||||
|
-- 공통코드 WAIVER_STATUS
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. chargeback_waiver_request (환수 감면 신청)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE TABLE chargeback_waiver_request (
|
||||||
|
request_id BIGSERIAL PRIMARY KEY,
|
||||||
|
contract_id BIGINT NOT NULL REFERENCES contract(contract_id),
|
||||||
|
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
|
||||||
|
settle_month CHAR(6) NOT NULL,
|
||||||
|
original_chargeback NUMERIC(15,2) NOT NULL,
|
||||||
|
waiver_amount NUMERIC(15,2) NOT NULL,
|
||||||
|
reason VARCHAR(200),
|
||||||
|
approval_status VARCHAR(20) NOT NULL DEFAULT 'REQUESTED',
|
||||||
|
approver_id BIGINT,
|
||||||
|
approved_at TIMESTAMP,
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||||
|
created_by BIGINT,
|
||||||
|
updated_at TIMESTAMP,
|
||||||
|
CONSTRAINT chk_cwr_original_nn CHECK (original_chargeback >= 0),
|
||||||
|
CONSTRAINT chk_cwr_waiver_nn CHECK (waiver_amount >= 0),
|
||||||
|
CONSTRAINT chk_cwr_waiver_lte_orig CHECK (waiver_amount <= original_chargeback),
|
||||||
|
CONSTRAINT chk_cwr_status CHECK (approval_status IN ('REQUESTED','APPROVED','REJECTED'))
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE chargeback_waiver_request IS '환수 감면 신청';
|
||||||
|
COMMENT ON COLUMN chargeback_waiver_request.request_id IS 'PK';
|
||||||
|
COMMENT ON COLUMN chargeback_waiver_request.contract_id IS '계약 ID';
|
||||||
|
COMMENT ON COLUMN chargeback_waiver_request.agent_id IS '설계사 ID';
|
||||||
|
COMMENT ON COLUMN chargeback_waiver_request.settle_month IS '정산월 YYYYMM';
|
||||||
|
COMMENT ON COLUMN chargeback_waiver_request.original_chargeback IS '원래 환수액';
|
||||||
|
COMMENT ON COLUMN chargeback_waiver_request.waiver_amount IS '감면 신청 금액 (<=original_chargeback)';
|
||||||
|
COMMENT ON COLUMN chargeback_waiver_request.reason IS '감면 사유';
|
||||||
|
COMMENT ON COLUMN chargeback_waiver_request.approval_status IS '승인 상태: REQUESTED/APPROVED/REJECTED';
|
||||||
|
COMMENT ON COLUMN chargeback_waiver_request.approver_id IS '승인자 사용자 ID';
|
||||||
|
COMMENT ON COLUMN chargeback_waiver_request.approved_at IS '승인/반려 일시';
|
||||||
|
|
||||||
|
CREATE INDEX idx_cwr_agent_month ON chargeback_waiver_request(agent_id, settle_month);
|
||||||
|
CREATE INDEX idx_cwr_status ON chargeback_waiver_request(approval_status);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. 메뉴 INSERT: CHARGEBACK_WAIVER (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
|
||||||
|
('CHARGEBACK_WAIVER', '환수감면신청', '/commission/chargeback-waiver', 'commission/ChargebackWaiver', 190)
|
||||||
|
) 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);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. 메뉴 권한 항목 등록 (READ/CREATE/UPDATE/DELETE/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),
|
||||||
|
('CREATE', '등록', 20),
|
||||||
|
('UPDATE', '수정', 30),
|
||||||
|
('DELETE', '삭제', 40),
|
||||||
|
('EXPORT', '엑셀', 60)
|
||||||
|
) AS p(code, name, sort)
|
||||||
|
WHERE m.menu_code = 'CHARGEBACK_WAIVER'
|
||||||
|
ON CONFLICT (menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 역할별 권한 부여
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
-- SUPER_ADMIN / ADMIN — 전체 권한
|
||||||
|
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 = 'CHARGEBACK_WAIVER'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- MANAGER — READ/CREATE/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 = 'MANAGER'
|
||||||
|
AND mp.perm_code IN ('READ','CREATE','UPDATE','EXPORT')
|
||||||
|
AND m.menu_code = 'CHARGEBACK_WAIVER'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- AGENT — 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 = 'AGENT'
|
||||||
|
AND mp.perm_code = 'READ'
|
||||||
|
AND m.menu_code = 'CHARGEBACK_WAIVER'
|
||||||
|
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 5. 공통코드: WAIVER_STATUS
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('WAIVER_STATUS', '환수감면상태', 'REQUESTED=신청/APPROVED=승인/REJECTED=반려', 'Y', 510)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, sort_order) VALUES
|
||||||
|
('WAIVER_STATUS', 'REQUESTED', '신청', 10),
|
||||||
|
('WAIVER_STATUS', 'APPROVED', '승인', 20),
|
||||||
|
('WAIVER_STATUS', 'REJECTED', '반려', 30)
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
-- V123: MOD-8 채널별 수수료 차등 (channel-specific commission)
|
||||||
|
-- contract.sales_channel 컬럼 추가
|
||||||
|
-- commission_rate.sales_channel 컬럼 추가 (V3 확인: product_id 기반 룰 테이블)
|
||||||
|
-- payout_rule.sales_channel 컬럼 추가 (V3 확인: grade_id 기반 룰 테이블)
|
||||||
|
-- 공통코드 SALES_CHANNEL
|
||||||
|
-- 하위호환: sales_channel NULL = DIRECT 취급, NULL 룰이 폴백
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 1. contract 테이블에 판매채널 컬럼 추가
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE contract ADD COLUMN IF NOT EXISTS sales_channel VARCHAR(20);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN contract.sales_channel IS '판매채널 (NULL=DIRECT 취급): DIRECT/BANCASSURANCE/HOME_SHOPPING/PARTNERSHIP';
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 2. commission_rate 테이블에 sales_channel 컬럼 추가
|
||||||
|
-- NULL = 모든 채널 적용 (폴백 룰)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE commission_rate ADD COLUMN IF NOT EXISTS sales_channel VARCHAR(20);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN commission_rate.sales_channel IS '판매채널 조건 (NULL=모든 채널 폴백)';
|
||||||
|
|
||||||
|
CREATE INDEX idx_cr_product_channel ON commission_rate(product_id, sales_channel, effective_from);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 3. payout_rule 테이블에 sales_channel 컬럼 추가
|
||||||
|
-- NULL = 모든 채널 적용 (폴백 룰)
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
ALTER TABLE payout_rule ADD COLUMN IF NOT EXISTS sales_channel VARCHAR(20);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN payout_rule.sales_channel IS '판매채널 조건 (NULL=모든 채널 폴백)';
|
||||||
|
|
||||||
|
CREATE INDEX idx_pr_grade_channel ON payout_rule(grade_id, insurance_type, sales_channel, effective_from);
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- 4. 공통코드: SALES_CHANNEL
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
|
||||||
|
('SALES_CHANNEL', '판매채널', 'DIRECT=직판/BANCASSURANCE=방카/HOME_SHOPPING=홈쇼핑/PARTNERSHIP=제휴', 'Y', 520)
|
||||||
|
ON CONFLICT (group_code) DO NOTHING;
|
||||||
|
|
||||||
|
INSERT INTO common_code (group_code, code, code_name, sort_order) VALUES
|
||||||
|
('SALES_CHANNEL', 'DIRECT', '직판', 10),
|
||||||
|
('SALES_CHANNEL', 'BANCASSURANCE', '방카', 20),
|
||||||
|
('SALES_CHANNEL', 'HOME_SHOPPING', '홈쇼핑', 30),
|
||||||
|
('SALES_CHANNEL', 'PARTNERSHIP', '제휴', 40)
|
||||||
|
ON CONFLICT (group_code, code) DO NOTHING;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.ga.core.mapper.commission;
|
||||||
|
|
||||||
|
import com.ga.core.vo.commission.AdvancePaymentResp;
|
||||||
|
import com.ga.core.vo.commission.AdvancePaymentSearchParam;
|
||||||
|
import com.ga.core.vo.commission.AdvancePaymentVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* advance_payment Mapper — 선지급금 마스터 (V117)
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface AdvancePaymentMapper {
|
||||||
|
|
||||||
|
AdvancePaymentResp selectById(@Param("advanceId") Long advanceId);
|
||||||
|
|
||||||
|
List<AdvancePaymentResp> selectList(AdvancePaymentSearchParam param);
|
||||||
|
|
||||||
|
int insert(AdvancePaymentVO vo);
|
||||||
|
|
||||||
|
int update(AdvancePaymentVO vo);
|
||||||
|
|
||||||
|
int delete(@Param("advanceId") Long advanceId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 설계사의 미상환 잔액 합계.
|
||||||
|
* status IN ('OUTSTANDING','PARTIAL') 대상 SUM(balance).
|
||||||
|
*/
|
||||||
|
BigDecimal outstandingByAgent(@Param("agentId") Long agentId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 설계사의 FIFO 상계 대상 선지급금 목록.
|
||||||
|
* status IN ('OUTSTANDING','PARTIAL') ORDER BY advance_month ASC, advance_id ASC.
|
||||||
|
*/
|
||||||
|
List<AdvancePaymentVO> selectOutstandingFifo(@Param("agentId") Long agentId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.ga.core.mapper.commission;
|
||||||
|
|
||||||
|
import com.ga.core.vo.commission.AdvanceRecoveryLedgerVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* advance_recovery_ledger Mapper — 선지급 상계 이력 (V117)
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface AdvanceRecoveryLedgerMapper {
|
||||||
|
|
||||||
|
int insert(AdvanceRecoveryLedgerVO vo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 배치 멱등용: 당월 상계 이력 삭제 (역산 후 재적용 패턴).
|
||||||
|
*/
|
||||||
|
int deleteBySettleMonth(@Param("advanceId") Long advanceId,
|
||||||
|
@Param("settleMonth") String settleMonth);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 선지급금 ID + 정산월 기준 상계 이력 조회.
|
||||||
|
*/
|
||||||
|
List<AdvanceRecoveryLedgerVO> selectBySettleMonth(@Param("advanceId") Long advanceId,
|
||||||
|
@Param("settleMonth") String settleMonth);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 배치 멱등용: 해당 정산월에 상계 이력이 있는 선지급금 ID 목록(중복 제거).
|
||||||
|
* reverse 단계에서 월 단위로 역산 대상을 식별한다.
|
||||||
|
*/
|
||||||
|
List<Long> selectAdvanceIdsBySettleMonth(@Param("settleMonth") String settleMonth);
|
||||||
|
}
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ga.core.mapper.commission;
|
||||||
|
|
||||||
|
import com.ga.core.vo.commission.CarrierReconciliationInResp;
|
||||||
|
import com.ga.core.vo.commission.CarrierReconciliationInSearchParam;
|
||||||
|
import com.ga.core.vo.commission.CarrierReconciliationInVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* carrier_reconciliation_in Mapper — 보험사 정산대사 입수 마스터 (V120)
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface CarrierReconciliationInMapper {
|
||||||
|
|
||||||
|
CarrierReconciliationInResp selectById(@Param("reconId") Long reconId);
|
||||||
|
|
||||||
|
List<CarrierReconciliationInResp> selectList(CarrierReconciliationInSearchParam param);
|
||||||
|
|
||||||
|
int insert(CarrierReconciliationInVO vo);
|
||||||
|
|
||||||
|
int update(CarrierReconciliationInVO vo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 보험사 + 정산월 기준 upsert — UNIQUE(company_id, settle_month) 충돌 시 갱신.
|
||||||
|
*/
|
||||||
|
int upsert(CarrierReconciliationInVO vo);
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ga.core.mapper.commission;
|
||||||
|
|
||||||
|
import com.ga.core.vo.commission.ChargebackWaiverRequestResp;
|
||||||
|
import com.ga.core.vo.commission.ChargebackWaiverRequestSearchParam;
|
||||||
|
import com.ga.core.vo.commission.ChargebackWaiverRequestVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chargeback_waiver_request Mapper — 환수 감면 신청 (V122)
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface ChargebackWaiverRequestMapper {
|
||||||
|
|
||||||
|
ChargebackWaiverRequestResp selectById(@Param("requestId") Long requestId);
|
||||||
|
|
||||||
|
List<ChargebackWaiverRequestResp> selectList(ChargebackWaiverRequestSearchParam param);
|
||||||
|
|
||||||
|
int insert(ChargebackWaiverRequestVO vo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 승인/반려 처리 — approval_status, approver_id, approved_at, updated_at 갱신.
|
||||||
|
*/
|
||||||
|
int updateApproval(@Param("requestId") Long requestId,
|
||||||
|
@Param("approvalStatus") String approvalStatus,
|
||||||
|
@Param("approverId") Long approverId,
|
||||||
|
@Param("approvedAt") LocalDateTime approvedAt);
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package com.ga.core.mapper.commission;
|
||||||
|
|
||||||
|
import com.ga.core.vo.commission.ClawbackInstallmentPlanResp;
|
||||||
|
import com.ga.core.vo.commission.ClawbackInstallmentPlanSearchParam;
|
||||||
|
import com.ga.core.vo.commission.ClawbackInstallmentPlanVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clawback_installment_plan Mapper — 환수 분할상환 계획 (V118)
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface ClawbackInstallmentPlanMapper {
|
||||||
|
|
||||||
|
ClawbackInstallmentPlanResp selectById(@Param("planId") Long planId);
|
||||||
|
|
||||||
|
List<ClawbackInstallmentPlanResp> selectList(ClawbackInstallmentPlanSearchParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 채권 ID 기준 분할상환 계획 목록 — 회차순 정렬.
|
||||||
|
*/
|
||||||
|
List<ClawbackInstallmentPlanVO> selectByReceivable(@Param("receivableId") Long receivableId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 분할상환 계획 일괄 등록.
|
||||||
|
*/
|
||||||
|
int insertBatch(List<ClawbackInstallmentPlanVO> plans);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 분할상환 회차 상태 갱신 (SCHEDULED→RECOVERED 또는 CANCELLED).
|
||||||
|
*/
|
||||||
|
int updateStatus(@Param("planId") Long planId,
|
||||||
|
@Param("status") String status);
|
||||||
|
}
|
||||||
@@ -77,4 +77,20 @@ public interface ClawbackReceivableMapper {
|
|||||||
List<Map<String, Object>> summaryByAgent(ClawbackReceivableSearchParam param);
|
List<Map<String, Object>> summaryByAgent(ClawbackReceivableSearchParam param);
|
||||||
|
|
||||||
int countByCondition(ClawbackReceivableSearchParam param);
|
int countByCondition(ClawbackReceivableSearchParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [MOD-6] 시효 만료 대상 채권 조회.
|
||||||
|
* origin_settle_month <= beforeMonth AND status IN('OUTSTANDING','RECOVERING')
|
||||||
|
* beforeMonth = baseMonth - expiration_months (Service에서 계산 후 전달).
|
||||||
|
*/
|
||||||
|
List<ClawbackReceivableVO> selectExpired(@Param("beforeMonth") String beforeMonth);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [MOD-6] 만료 대상 채권 일괄 대손 처리.
|
||||||
|
* status='WRITTEN_OFF', writeoff_reason=reason, written_off_at=NOW(), updated_at=NOW()
|
||||||
|
* 대상: origin_settle_month <= beforeMonth AND status IN('OUTSTANDING','RECOVERING')
|
||||||
|
* 처리 건수 반환.
|
||||||
|
*/
|
||||||
|
int writeOffExpired(@Param("beforeMonth") String beforeMonth,
|
||||||
|
@Param("reason") String reason);
|
||||||
}
|
}
|
||||||
|
|||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ga.core.mapper.commission;
|
||||||
|
|
||||||
|
import com.ga.core.vo.commission.LapsedReinstatementRecoveryResp;
|
||||||
|
import com.ga.core.vo.commission.LapsedReinstatementRecoverySearchParam;
|
||||||
|
import com.ga.core.vo.commission.LapsedReinstatementRecoveryVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lapsed_reinstatement_recovery Mapper — 익월 부활 재지급 원장 (V119)
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface LapsedReinstatementRecoveryMapper {
|
||||||
|
|
||||||
|
LapsedReinstatementRecoveryResp selectById(@Param("recoveryId") Long recoveryId);
|
||||||
|
|
||||||
|
List<LapsedReinstatementRecoveryResp> selectList(LapsedReinstatementRecoverySearchParam param);
|
||||||
|
|
||||||
|
int insert(LapsedReinstatementRecoveryVO vo);
|
||||||
|
|
||||||
|
int updateStatus(@Param("recoveryId") Long recoveryId,
|
||||||
|
@Param("status") String status,
|
||||||
|
@Param("repaidAmount") java.math.BigDecimal repaidAmount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 정산월 기준 설계사별 재지급 합계 — AggregateStep other_commission_total 가산용.
|
||||||
|
* status='REPAID' 대상 SUM(repaid_amount) GROUP BY agent_id.
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ga.core.mapper.commission;
|
||||||
|
|
||||||
|
import com.ga.core.vo.commission.ReconciliationVarianceVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reconciliation_variance Mapper — 대사 차이 상세 (V120)
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface ReconciliationVarianceMapper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 차이 상세 일괄 등록.
|
||||||
|
*/
|
||||||
|
int insertBatch(List<ReconciliationVarianceVO> variances);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 대사 입수 ID 기준 차이 상세 목록 조회.
|
||||||
|
*/
|
||||||
|
List<ReconciliationVarianceVO> selectByRecon(@Param("reconId") Long reconId);
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ga.core.mapper.commission;
|
||||||
|
|
||||||
|
import com.ga.core.vo.commission.RecruiterCommissionLedgerResp;
|
||||||
|
import com.ga.core.vo.commission.RecruiterCommissionLedgerSearchParam;
|
||||||
|
import com.ga.core.vo.commission.RecruiterCommissionLedgerVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* recruiter_commission_ledger Mapper — 도입수수료 원장 (V116)
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface RecruiterCommissionLedgerMapper {
|
||||||
|
|
||||||
|
RecruiterCommissionLedgerResp selectById(@Param("ledgerId") Long ledgerId);
|
||||||
|
|
||||||
|
List<RecruiterCommissionLedgerResp> selectList(RecruiterCommissionLedgerSearchParam param);
|
||||||
|
|
||||||
|
int insert(RecruiterCommissionLedgerVO vo);
|
||||||
|
|
||||||
|
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
|
||||||
|
|
||||||
|
int delete(@Param("ledgerId") Long ledgerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 정산월 기준 도입자 설계사별 도입수수료 합계 — AggregateStep other_commission_total 가산용.
|
||||||
|
* recruiter_agent_id 기준으로 agentId alias, CANCELLED 제외.
|
||||||
|
*/
|
||||||
|
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
package com.ga.core.mapper.commission;
|
||||||
|
|
||||||
|
import com.ga.core.vo.commission.RecruiterCommissionRuleResp;
|
||||||
|
import com.ga.core.vo.commission.RecruiterCommissionRuleSearchParam;
|
||||||
|
import com.ga.core.vo.commission.RecruiterCommissionRuleVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* recruiter_commission_rule Mapper — 도입수수료 룰 마스터 (V116)
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface RecruiterCommissionRuleMapper {
|
||||||
|
|
||||||
|
RecruiterCommissionRuleResp selectById(@Param("ruleId") Long ruleId);
|
||||||
|
|
||||||
|
List<RecruiterCommissionRuleResp> selectList(RecruiterCommissionRuleSearchParam param);
|
||||||
|
|
||||||
|
int insert(RecruiterCommissionRuleVO vo);
|
||||||
|
|
||||||
|
int update(RecruiterCommissionRuleVO vo);
|
||||||
|
|
||||||
|
int delete(@Param("ruleId") Long ruleId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.mapper.commission;
|
||||||
|
|
||||||
|
import com.ga.core.vo.commission.StatuteConfigResp;
|
||||||
|
import com.ga.core.vo.commission.StatuteConfigVO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* statute_config Mapper — 환수 시효 설정 (V121)
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface StatuteConfigMapper {
|
||||||
|
|
||||||
|
StatuteConfigResp selectById(@Param("configId") Long configId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* entity_type 기준 단건 조회 (UNIQUE).
|
||||||
|
*/
|
||||||
|
StatuteConfigResp selectByEntityType(@Param("entityType") String entityType);
|
||||||
|
|
||||||
|
List<StatuteConfigResp> selectList();
|
||||||
|
|
||||||
|
int insert(StatuteConfigVO vo);
|
||||||
|
|
||||||
|
int update(StatuteConfigVO vo);
|
||||||
|
}
|
||||||
@@ -17,10 +17,11 @@ public interface RuleMapper {
|
|||||||
// === commission_rate ===
|
// === commission_rate ===
|
||||||
List<CommissionRateVO> selectCommissionRates(@Param("productId") Long productId,
|
List<CommissionRateVO> selectCommissionRates(@Param("productId") Long productId,
|
||||||
@Param("year") Integer year);
|
@Param("year") Integer year);
|
||||||
/** 계약일에 유효한 보험사 수수료율 */
|
/** 계약일에 유효한 보험사 수수료율 (판매채널 우선, NULL=모든채널 폴백) */
|
||||||
BigDecimal findCompanyRate(@Param("productId") Long productId,
|
BigDecimal findCompanyRate(@Param("productId") Long productId,
|
||||||
@Param("year") Integer year,
|
@Param("year") Integer year,
|
||||||
@Param("baseDate") LocalDate baseDate);
|
@Param("baseDate") LocalDate baseDate,
|
||||||
|
@Param("salesChannel") String salesChannel);
|
||||||
int insertCommissionRate(CommissionRateVO vo);
|
int insertCommissionRate(CommissionRateVO vo);
|
||||||
int updateCommissionRate(CommissionRateVO vo);
|
int updateCommissionRate(CommissionRateVO vo);
|
||||||
int deleteCommissionRate(@Param("rateId") Long rateId);
|
int deleteCommissionRate(@Param("rateId") Long rateId);
|
||||||
@@ -31,7 +32,8 @@ public interface RuleMapper {
|
|||||||
BigDecimal findPayoutRate(@Param("gradeId") Long gradeId,
|
BigDecimal findPayoutRate(@Param("gradeId") Long gradeId,
|
||||||
@Param("insuranceType") String insuranceType,
|
@Param("insuranceType") String insuranceType,
|
||||||
@Param("year") Integer year,
|
@Param("year") Integer year,
|
||||||
@Param("baseDate") LocalDate baseDate);
|
@Param("baseDate") LocalDate baseDate,
|
||||||
|
@Param("salesChannel") String salesChannel);
|
||||||
int insertPayoutRule(PayoutRuleVO vo);
|
int insertPayoutRule(PayoutRuleVO vo);
|
||||||
int updatePayoutRule(PayoutRuleVO vo);
|
int updatePayoutRule(PayoutRuleVO vo);
|
||||||
int deletePayoutRule(@Param("ruleId") Long ruleId);
|
int deletePayoutRule(@Param("ruleId") Long ruleId);
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* advance_payment 조회 응답 — 설계사명 조인 포함 (V117)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AdvancePaymentResp {
|
||||||
|
private Long advanceId;
|
||||||
|
private Long agentId;
|
||||||
|
/** 설계사명 */
|
||||||
|
private String agentName;
|
||||||
|
/** 조직명 */
|
||||||
|
private String orgName;
|
||||||
|
private String advanceMonth;
|
||||||
|
private BigDecimal principalAmount;
|
||||||
|
private BigDecimal recoveredAmount;
|
||||||
|
private BigDecimal balance;
|
||||||
|
private String status;
|
||||||
|
/** 공통코드 ADVANCE_STATUS */
|
||||||
|
private String statusName;
|
||||||
|
private String memo;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* advance_payment 등록 요청 (V117)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AdvancePaymentSaveReq {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long agentId;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String advanceMonth;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin(value = "0", inclusive = false)
|
||||||
|
private BigDecimal principalAmount;
|
||||||
|
|
||||||
|
private String memo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* advance_payment 검색 조건 (V117)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class AdvancePaymentSearchParam extends SearchParam {
|
||||||
|
private Long agentId;
|
||||||
|
private String agentName;
|
||||||
|
private String advanceMonth;
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* advance_payment — 선지급금 마스터 (V117)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AdvancePaymentVO {
|
||||||
|
private Long advanceId;
|
||||||
|
private Long agentId;
|
||||||
|
/** 선지급 발생월 YYYYMM */
|
||||||
|
private String advanceMonth;
|
||||||
|
/** 원금 */
|
||||||
|
private BigDecimal principalAmount;
|
||||||
|
/** 회수 누계 */
|
||||||
|
private BigDecimal recoveredAmount;
|
||||||
|
/** 잔액 (principal - recovered) */
|
||||||
|
private BigDecimal balance;
|
||||||
|
/** 상태: OUTSTANDING/PARTIAL/CLEARED/WAIVED */
|
||||||
|
private String status;
|
||||||
|
private String memo;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* advance_recovery_ledger — 선지급 상계 이력 (V117)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AdvanceRecoveryLedgerVO {
|
||||||
|
private Long recoveryId;
|
||||||
|
private Long advanceId;
|
||||||
|
/** 상계 정산월 YYYYMM */
|
||||||
|
private String settleMonth;
|
||||||
|
/** 당월 상계 금액 */
|
||||||
|
private BigDecimal recoveryAmount;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 선지급 상태 (ADVANCE_STATUS 그룹, V117)
|
||||||
|
*/
|
||||||
|
public enum AdvanceStatus {
|
||||||
|
OUTSTANDING("미상환"),
|
||||||
|
PARTIAL("부분상환"),
|
||||||
|
CLEARED("상환완료"),
|
||||||
|
WAIVED("면제");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
AdvanceStatus(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonValue
|
||||||
|
public String getCode() {
|
||||||
|
return this.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* carrier_reconciliation_in 조회 응답 — 보험사명 조인 포함 (V120)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CarrierReconciliationInResp {
|
||||||
|
private Long reconId;
|
||||||
|
private Long companyId;
|
||||||
|
/** 보험사 코드 */
|
||||||
|
private String companyCode;
|
||||||
|
/** 보험사명 */
|
||||||
|
private String companyName;
|
||||||
|
private String settleMonth;
|
||||||
|
private String fileName;
|
||||||
|
private Integer recordCount;
|
||||||
|
private BigDecimal reportedTotal;
|
||||||
|
private BigDecimal systemTotal;
|
||||||
|
private BigDecimal varianceAmount;
|
||||||
|
private String status;
|
||||||
|
/** 공통코드 RECON_IN_STATUS */
|
||||||
|
private String statusName;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* carrier_reconciliation_in 등록/수정 요청 (V120)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CarrierReconciliationInSaveReq {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long companyId;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String settleMonth;
|
||||||
|
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
private Integer recordCount;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin("0")
|
||||||
|
private BigDecimal reportedTotal;
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* carrier_reconciliation_in 검색 조건 (V120)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class CarrierReconciliationInSearchParam extends SearchParam {
|
||||||
|
private Long companyId;
|
||||||
|
private String settleMonth;
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* carrier_reconciliation_in — 보험사 정산대사 입수 마스터 (V120)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class CarrierReconciliationInVO {
|
||||||
|
private Long reconId;
|
||||||
|
private Long companyId;
|
||||||
|
/** 정산월 YYYYMM */
|
||||||
|
private String settleMonth;
|
||||||
|
private String fileName;
|
||||||
|
private Integer recordCount;
|
||||||
|
/** 보험사 통보 금액 합계 */
|
||||||
|
private BigDecimal reportedTotal;
|
||||||
|
/** GA 시스템 집계 금액 */
|
||||||
|
private BigDecimal systemTotal;
|
||||||
|
/** 차이 금액 (reported - system) */
|
||||||
|
private BigDecimal varianceAmount;
|
||||||
|
/** 상태: RECEIVED/MATCHED/VARIANCE/APPROVED */
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 환수 분할상환 상태 (CB_INSTALLMENT_STATUS 그룹, V118)
|
||||||
|
*/
|
||||||
|
public enum CbInstallmentStatus {
|
||||||
|
SCHEDULED("예정"),
|
||||||
|
RECOVERED("회수"),
|
||||||
|
CANCELLED("취소");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
CbInstallmentStatus(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonValue
|
||||||
|
public String getCode() {
|
||||||
|
return this.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chargeback_waiver_request 조회 응답 — 설계사명·계약정보 조인 포함 (V122)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ChargebackWaiverRequestResp {
|
||||||
|
private Long requestId;
|
||||||
|
private Long contractId;
|
||||||
|
/** 증권번호 */
|
||||||
|
private String policyNo;
|
||||||
|
private Long agentId;
|
||||||
|
/** 설계사명 */
|
||||||
|
private String agentName;
|
||||||
|
/** 조직명 */
|
||||||
|
private String orgName;
|
||||||
|
private String settleMonth;
|
||||||
|
private BigDecimal originalChargeback;
|
||||||
|
private BigDecimal waiverAmount;
|
||||||
|
private String reason;
|
||||||
|
private String approvalStatus;
|
||||||
|
/** 공통코드 WAIVER_STATUS */
|
||||||
|
private String approvalStatusName;
|
||||||
|
private Long approverId;
|
||||||
|
private LocalDateTime approvedAt;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chargeback_waiver_request 등록 요청 (V122).
|
||||||
|
* waiver_amount <= original_chargeback 검증은 Service에서 수행.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ChargebackWaiverRequestSaveReq {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long contractId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long agentId;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String settleMonth;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin("0")
|
||||||
|
private BigDecimal originalChargeback;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin("0")
|
||||||
|
private BigDecimal waiverAmount;
|
||||||
|
|
||||||
|
private String reason;
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chargeback_waiver_request 검색 조건 (V122)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ChargebackWaiverRequestSearchParam extends SearchParam {
|
||||||
|
private Long agentId;
|
||||||
|
private String agentName;
|
||||||
|
private String settleMonth;
|
||||||
|
private String approvalStatus;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* chargeback_waiver_request — 환수 감면 신청 (V122)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ChargebackWaiverRequestVO {
|
||||||
|
private Long requestId;
|
||||||
|
private Long contractId;
|
||||||
|
private Long agentId;
|
||||||
|
/** 정산월 YYYYMM */
|
||||||
|
private String settleMonth;
|
||||||
|
/** 원래 환수액 */
|
||||||
|
private BigDecimal originalChargeback;
|
||||||
|
/** 감면 신청 금액 (<=originalChargeback) */
|
||||||
|
private BigDecimal waiverAmount;
|
||||||
|
/** 감면 사유 */
|
||||||
|
private String reason;
|
||||||
|
/** 승인 상태: REQUESTED/APPROVED/REJECTED */
|
||||||
|
private String approvalStatus;
|
||||||
|
/** 승인자 사용자 ID */
|
||||||
|
private Long approverId;
|
||||||
|
/** 승인/반려 일시 */
|
||||||
|
private LocalDateTime approvedAt;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clawback_installment_plan 조회 응답 — 채권 정보 조인 포함 (V118)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ClawbackInstallmentPlanResp {
|
||||||
|
private Long planId;
|
||||||
|
private Long receivableId;
|
||||||
|
/** 채권 발생월 (clawback_receivable.origin_settle_month) */
|
||||||
|
private String originSettleMonth;
|
||||||
|
/** 채권 설계사 ID */
|
||||||
|
private Long agentId;
|
||||||
|
/** 채권 설계사명 */
|
||||||
|
private String agentName;
|
||||||
|
private Integer installmentNo;
|
||||||
|
private BigDecimal installmentAmount;
|
||||||
|
private String dueMonth;
|
||||||
|
private String status;
|
||||||
|
/** 공통코드 CB_INSTALLMENT_STATUS */
|
||||||
|
private String statusName;
|
||||||
|
private LocalDateTime recoveredAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clawback_installment_plan 단건 등록 요청 (V118).
|
||||||
|
* 일괄 생성은 Service의 generate(receivableId, count) 사용.
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ClawbackInstallmentPlanSaveReq {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long receivableId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Min(1)
|
||||||
|
private Integer installmentNo;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin(value = "0", inclusive = false)
|
||||||
|
private BigDecimal installmentAmount;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String dueMonth;
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clawback_installment_plan 검색 조건 (V118)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class ClawbackInstallmentPlanSearchParam extends SearchParam {
|
||||||
|
private Long receivableId;
|
||||||
|
private Long agentId;
|
||||||
|
private String dueMonth;
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clawback_installment_plan — 환수 분할상환 계획 (V118)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ClawbackInstallmentPlanVO {
|
||||||
|
private Long planId;
|
||||||
|
private Long receivableId;
|
||||||
|
/** 분할 회차 번호 (1부터) */
|
||||||
|
private Integer installmentNo;
|
||||||
|
/** 회차 납부 금액 */
|
||||||
|
private BigDecimal installmentAmount;
|
||||||
|
/** 납부 예정월 YYYYMM */
|
||||||
|
private String dueMonth;
|
||||||
|
/** 상태: SCHEDULED/RECOVERED/CANCELLED */
|
||||||
|
private String status;
|
||||||
|
/** 실제 회수 일시 */
|
||||||
|
private LocalDateTime recoveredAt;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 익월 부활 재지급 상태 (LAPSE_RECOVERY_STATUS 그룹, V119)
|
||||||
|
*/
|
||||||
|
public enum LapseRecoveryStatus {
|
||||||
|
PENDING("대기"),
|
||||||
|
REPAID("재지급완료"),
|
||||||
|
CANCELLED("취소");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
LapseRecoveryStatus(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonValue
|
||||||
|
public String getCode() {
|
||||||
|
return this.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lapsed_reinstatement_recovery 조회 응답 — 설계사명·계약정보 조인 포함 (V119)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class LapsedReinstatementRecoveryResp {
|
||||||
|
private Long recoveryId;
|
||||||
|
private Long contractId;
|
||||||
|
/** 증권번호 */
|
||||||
|
private String policyNo;
|
||||||
|
private Long agentId;
|
||||||
|
/** 설계사명 */
|
||||||
|
private String agentName;
|
||||||
|
/** 조직명 */
|
||||||
|
private String orgName;
|
||||||
|
private String lapsedMonth;
|
||||||
|
private String reinstatedMonth;
|
||||||
|
private BigDecimal withheldAmount;
|
||||||
|
private BigDecimal repaidAmount;
|
||||||
|
private String status;
|
||||||
|
/** 공통코드 LAPSE_RECOVERY_STATUS */
|
||||||
|
private String statusName;
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lapsed_reinstatement_recovery 등록 요청 (V119)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class LapsedReinstatementRecoverySaveReq {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long contractId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long agentId;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String lapsedMonth;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String reinstatedMonth;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin("0")
|
||||||
|
private BigDecimal withheldAmount;
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lapsed_reinstatement_recovery 검색 조건 (V119)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class LapsedReinstatementRecoverySearchParam extends SearchParam {
|
||||||
|
private Long agentId;
|
||||||
|
private String agentName;
|
||||||
|
private String reinstatedMonth;
|
||||||
|
private String lapsedMonth;
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* lapsed_reinstatement_recovery — 익월 부활 재지급 원장 (V119)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class LapsedReinstatementRecoveryVO {
|
||||||
|
private Long recoveryId;
|
||||||
|
private Long contractId;
|
||||||
|
private Long agentId;
|
||||||
|
/** 실효 발생월 YYYYMM */
|
||||||
|
private String lapsedMonth;
|
||||||
|
/** 부활 확정월 YYYYMM */
|
||||||
|
private String reinstatedMonth;
|
||||||
|
/** 보류된 수수료 금액 */
|
||||||
|
private BigDecimal withheldAmount;
|
||||||
|
/** 재지급 완료 금액 */
|
||||||
|
private BigDecimal repaidAmount;
|
||||||
|
/** 상태: PENDING/REPAID/CANCELLED */
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 정산대사 입수 상태 (RECON_IN_STATUS 그룹, V120)
|
||||||
|
*/
|
||||||
|
public enum ReconInStatus {
|
||||||
|
RECEIVED("입수"),
|
||||||
|
MATCHED("일치"),
|
||||||
|
VARIANCE("차이"),
|
||||||
|
APPROVED("승인");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
ReconInStatus(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonValue
|
||||||
|
public String getCode() {
|
||||||
|
return this.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* reconciliation_variance — 대사 차이 상세 (V120)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class ReconciliationVarianceVO {
|
||||||
|
private Long varianceId;
|
||||||
|
private Long reconId;
|
||||||
|
/** 증권번호 */
|
||||||
|
private String policyNo;
|
||||||
|
/** GA 시스템 예상 금액 */
|
||||||
|
private BigDecimal expectedAmount;
|
||||||
|
/** 보험사 통보 금액 */
|
||||||
|
private BigDecimal reportedAmount;
|
||||||
|
/** 차이 사유 */
|
||||||
|
private String varianceReason;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* recruiter_commission_ledger 조회 응답 — 설계사명·공통코드명 조인 포함 (V116)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RecruiterCommissionLedgerResp {
|
||||||
|
private Long ledgerId;
|
||||||
|
private Long recruiterAgentId;
|
||||||
|
/** 도입자 설계사명 */
|
||||||
|
private String recruiterAgentName;
|
||||||
|
private Long recruitAgentId;
|
||||||
|
/** 피도입 설계사명 */
|
||||||
|
private String recruitAgentName;
|
||||||
|
private String settleMonth;
|
||||||
|
private String recruiterType;
|
||||||
|
/** 공통코드 RECRUITER_TYPE */
|
||||||
|
private String recruiterTypeName;
|
||||||
|
private BigDecimal baseAmount;
|
||||||
|
private BigDecimal commissionAmount;
|
||||||
|
private String status;
|
||||||
|
/** 공통코드 상태명 */
|
||||||
|
private String statusName;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* recruiter_commission_ledger 등록 요청 (V116)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RecruiterCommissionLedgerSaveReq {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long recruiterAgentId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long recruitAgentId;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String settleMonth;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String recruiterType;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin("0")
|
||||||
|
private BigDecimal baseAmount;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@DecimalMin("0")
|
||||||
|
private BigDecimal commissionAmount;
|
||||||
|
}
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* recruiter_commission_ledger 검색 조건 (V116)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class RecruiterCommissionLedgerSearchParam extends SearchParam {
|
||||||
|
private Long recruiterAgentId;
|
||||||
|
private Long recruitAgentId;
|
||||||
|
private String settleMonth;
|
||||||
|
private String recruiterType;
|
||||||
|
private String status;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* recruiter_commission_ledger — 도입수수료 원장 (V116)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RecruiterCommissionLedgerVO {
|
||||||
|
private Long ledgerId;
|
||||||
|
private Long recruiterAgentId;
|
||||||
|
private Long recruitAgentId;
|
||||||
|
private String settleMonth;
|
||||||
|
/** RecruiterType: NEW_RECRUIT/DEVELOPMENT/TRAINING */
|
||||||
|
private String recruiterType;
|
||||||
|
/** 기준금액 (피도입 설계사 당월 모집수수료 합) */
|
||||||
|
private BigDecimal baseAmount;
|
||||||
|
/** 도입수수료 금액 */
|
||||||
|
private BigDecimal commissionAmount;
|
||||||
|
/** 상태: CALCULATED/CONFIRMED/PAID/CANCELLED */
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* recruiter_commission_rule 조회 응답 — 공통코드명 조인 포함 (V116)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RecruiterCommissionRuleResp {
|
||||||
|
private Long ruleId;
|
||||||
|
private String recruiterType;
|
||||||
|
/** 공통코드 RECRUITER_TYPE */
|
||||||
|
private String recruiterTypeName;
|
||||||
|
private String calcType;
|
||||||
|
/** 공통코드 CALC_TYPE */
|
||||||
|
private String calcTypeName;
|
||||||
|
private BigDecimal commissionRate;
|
||||||
|
private BigDecimal fixedAmount;
|
||||||
|
private LocalDate effectiveFrom;
|
||||||
|
private LocalDate effectiveTo;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.DecimalMax;
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* recruiter_commission_rule 등록/수정 요청 (V116)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RecruiterCommissionRuleSaveReq {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String recruiterType;
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String calcType;
|
||||||
|
|
||||||
|
@DecimalMin("0")
|
||||||
|
@DecimalMax("1")
|
||||||
|
private BigDecimal commissionRate;
|
||||||
|
|
||||||
|
@DecimalMin("0")
|
||||||
|
private BigDecimal fixedAmount;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private LocalDate effectiveFrom;
|
||||||
|
|
||||||
|
private LocalDate effectiveTo;
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.ga.common.model.SearchParam;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* recruiter_commission_rule 검색 조건 (V116)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class RecruiterCommissionRuleSearchParam extends SearchParam {
|
||||||
|
private String recruiterType;
|
||||||
|
private String calcType;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* recruiter_commission_rule — 도입수수료 룰 마스터 (V116)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RecruiterCommissionRuleVO {
|
||||||
|
private Long ruleId;
|
||||||
|
/** RecruiterType: NEW_RECRUIT/DEVELOPMENT/TRAINING */
|
||||||
|
private String recruiterType;
|
||||||
|
/** CalcType: RATE/FIXED */
|
||||||
|
private String calcType;
|
||||||
|
/** 정률 요율 (0~1, calc_type=RATE 일 때 사용) */
|
||||||
|
private BigDecimal commissionRate;
|
||||||
|
/** 정액 금액 (calc_type=FIXED 일 때 사용) */
|
||||||
|
private BigDecimal fixedAmount;
|
||||||
|
private LocalDate effectiveFrom;
|
||||||
|
private LocalDate effectiveTo;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 도입수수료 유형 (RECRUITER_TYPE 그룹, V116)
|
||||||
|
*/
|
||||||
|
public enum RecruiterType {
|
||||||
|
NEW_RECRUIT("신인도입"),
|
||||||
|
DEVELOPMENT("육성"),
|
||||||
|
TRAINING("교육");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
RecruiterType(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonValue
|
||||||
|
public String getCode() {
|
||||||
|
return this.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* statute_config 조회 응답 (V121)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class StatuteConfigResp {
|
||||||
|
private Long configId;
|
||||||
|
private String entityType;
|
||||||
|
private Integer expirationMonths;
|
||||||
|
private String isActive;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* statute_config 등록/수정 요청 (V121)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class StatuteConfigSaveReq {
|
||||||
|
|
||||||
|
@NotBlank
|
||||||
|
private String entityType;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Min(1)
|
||||||
|
private Integer expirationMonths;
|
||||||
|
|
||||||
|
private String isActive;
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* statute_config — 환수 시효 설정 (V121)
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class StatuteConfigVO {
|
||||||
|
private Long configId;
|
||||||
|
/** 대상 엔티티 타입 (CLAWBACK_RECEIVABLE) */
|
||||||
|
private String entityType;
|
||||||
|
/** 시효 만료 개월수 */
|
||||||
|
private Integer expirationMonths;
|
||||||
|
/** 활성 여부 (Y/N) */
|
||||||
|
private String isActive;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private Long createdBy;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ga.core.vo.commission;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 환수 감면 승인 상태 (WAIVER_STATUS 그룹, V122)
|
||||||
|
*/
|
||||||
|
public enum WaiverStatus {
|
||||||
|
REQUESTED("신청"),
|
||||||
|
APPROVED("승인"),
|
||||||
|
REJECTED("반려");
|
||||||
|
|
||||||
|
private final String label;
|
||||||
|
|
||||||
|
WaiverStatus(String label) {
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonValue
|
||||||
|
public String getCode() {
|
||||||
|
return this.name();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLabel() {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,4 +30,8 @@ public class ContractResp {
|
|||||||
private LocalDate contractDate;
|
private LocalDate contractDate;
|
||||||
private LocalDate effectiveDate;
|
private LocalDate effectiveDate;
|
||||||
private LocalDate lapseDate;
|
private LocalDate lapseDate;
|
||||||
|
/** 판매채널 (NULL=DIRECT 취급): DIRECT/BANCASSURANCE/HOME_SHOPPING/PARTNERSHIP (V123) */
|
||||||
|
private String salesChannel;
|
||||||
|
/** 공통코드 SALES_CHANNEL */
|
||||||
|
private String salesChannelName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,4 +23,6 @@ public class ContractSaveReq {
|
|||||||
private Integer coveragePeriod;
|
private Integer coveragePeriod;
|
||||||
@NotNull private LocalDate contractDate;
|
@NotNull private LocalDate contractDate;
|
||||||
private LocalDate effectiveDate;
|
private LocalDate effectiveDate;
|
||||||
|
/** 판매채널 (NULL=DIRECT): DIRECT/BANCASSURANCE/HOME_SHOPPING/PARTNERSHIP (V123) */
|
||||||
|
private String salesChannel;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ public class ContractVO {
|
|||||||
private LocalDate effectiveDate;
|
private LocalDate effectiveDate;
|
||||||
private LocalDate lapseDate;
|
private LocalDate lapseDate;
|
||||||
private LocalDate revivalDate;
|
private LocalDate revivalDate;
|
||||||
|
/** 판매채널 (NULL=DIRECT 취급): DIRECT/BANCASSURANCE/HOME_SHOPPING/PARTNERSHIP (V123) */
|
||||||
|
private String salesChannel;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.core.mapper.commission.AdvancePaymentMapper">
|
||||||
|
|
||||||
|
<resultMap id="VOMap" type="com.ga.core.vo.commission.AdvancePaymentVO"/>
|
||||||
|
<resultMap id="RespMap" type="com.ga.core.vo.commission.AdvancePaymentResp"/>
|
||||||
|
|
||||||
|
<!-- 목록/단건 조회 컬럼 — 설계사명·조직명·공통코드명 조인 포함 -->
|
||||||
|
<sql id="advJoinCols">
|
||||||
|
ap.advance_id,
|
||||||
|
ap.agent_id, a.agent_name, o.org_name,
|
||||||
|
ap.advance_month,
|
||||||
|
ap.principal_amount,
|
||||||
|
ap.recovered_amount,
|
||||||
|
ap.balance,
|
||||||
|
ap.status,
|
||||||
|
(SELECT cc.code_name FROM common_code cc
|
||||||
|
WHERE cc.group_code = 'ADVANCE_STATUS' AND cc.code = ap.status) AS status_name,
|
||||||
|
ap.memo,
|
||||||
|
ap.created_at
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<sql id="advJoins">
|
||||||
|
FROM advance_payment ap
|
||||||
|
LEFT JOIN agent a ON a.agent_id = ap.agent_id
|
||||||
|
LEFT JOIN organization o ON o.org_id = a.org_id
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="RespMap">
|
||||||
|
SELECT <include refid="advJoinCols"/>
|
||||||
|
<include refid="advJoins"/>
|
||||||
|
WHERE ap.advance_id = #{advanceId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="RespMap">
|
||||||
|
SELECT <include refid="advJoinCols"/>
|
||||||
|
<include refid="advJoins"/>
|
||||||
|
<where>
|
||||||
|
<if test="agentId != null">AND ap.agent_id = #{agentId}</if>
|
||||||
|
<if test="agentName != null and agentName != ''">AND a.agent_name LIKE '%' || #{agentName} || '%'</if>
|
||||||
|
<if test="advanceMonth != null and advanceMonth != ''">AND ap.advance_month = #{advanceMonth}</if>
|
||||||
|
<if test="status != null and status != ''">AND ap.status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY ap.advance_month DESC, ap.advance_id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- balance = principal_amount (초기 등록 시 잔액=원금) -->
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="advanceId">
|
||||||
|
INSERT INTO advance_payment
|
||||||
|
(agent_id, advance_month, principal_amount, recovered_amount, balance,
|
||||||
|
status, memo, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(#{agentId}, #{advanceMonth}, #{principalAmount}, 0, #{principalAmount},
|
||||||
|
'OUTSTANDING', #{memo}, #{createdBy}, NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 회수 후 갱신 — recovered_amount, balance, status, updated_at -->
|
||||||
|
<update id="update">
|
||||||
|
UPDATE advance_payment
|
||||||
|
SET recovered_amount = #{recoveredAmount},
|
||||||
|
balance = #{balance},
|
||||||
|
status = #{status},
|
||||||
|
memo = #{memo},
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE advance_id = #{advanceId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="delete">
|
||||||
|
DELETE FROM advance_payment WHERE advance_id = #{advanceId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<!-- 설계사의 미상환 잔액 합계 — OUTSTANDING/PARTIAL 대상 -->
|
||||||
|
<select id="outstandingByAgent" resultType="java.math.BigDecimal">
|
||||||
|
SELECT COALESCE(SUM(balance), 0)
|
||||||
|
FROM advance_payment
|
||||||
|
WHERE agent_id = #{agentId}
|
||||||
|
AND status IN ('OUTSTANDING', 'PARTIAL')
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- FIFO 상계 대상 목록 — 오래된 발생월 우선 -->
|
||||||
|
<select id="selectOutstandingFifo" resultMap="VOMap">
|
||||||
|
SELECT advance_id, agent_id, advance_month,
|
||||||
|
principal_amount, recovered_amount, balance,
|
||||||
|
status, memo, created_at, created_by, updated_at
|
||||||
|
FROM advance_payment
|
||||||
|
WHERE agent_id = #{agentId}
|
||||||
|
AND status IN ('OUTSTANDING', 'PARTIAL')
|
||||||
|
ORDER BY advance_month ASC, advance_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.core.mapper.commission.AdvanceRecoveryLedgerMapper">
|
||||||
|
|
||||||
|
<resultMap id="VOMap" type="com.ga.core.vo.commission.AdvanceRecoveryLedgerVO"/>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="recoveryId">
|
||||||
|
INSERT INTO advance_recovery_ledger
|
||||||
|
(advance_id, settle_month, recovery_amount, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(#{advanceId}, #{settleMonth}, #{recoveryAmount}, #{createdBy}, NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
배치 멱등용: 당월 상계 이력 삭제.
|
||||||
|
P9 reverseClawbackRecovery 패턴 — 재실행 시 역산 후 재적용.
|
||||||
|
-->
|
||||||
|
<delete id="deleteBySettleMonth">
|
||||||
|
DELETE FROM advance_recovery_ledger
|
||||||
|
WHERE advance_id = #{advanceId}
|
||||||
|
AND settle_month = #{settleMonth}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<select id="selectBySettleMonth" resultMap="VOMap">
|
||||||
|
SELECT recovery_id, advance_id, settle_month, recovery_amount,
|
||||||
|
created_at, created_by
|
||||||
|
FROM advance_recovery_ledger
|
||||||
|
WHERE advance_id = #{advanceId}
|
||||||
|
AND settle_month = #{settleMonth}
|
||||||
|
ORDER BY recovery_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectAdvanceIdsBySettleMonth" resultType="long">
|
||||||
|
SELECT DISTINCT advance_id
|
||||||
|
FROM advance_recovery_ledger
|
||||||
|
WHERE settle_month = #{settleMonth}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.core.mapper.commission.CarrierReconciliationInMapper">
|
||||||
|
|
||||||
|
<resultMap id="VOMap" type="com.ga.core.vo.commission.CarrierReconciliationInVO"/>
|
||||||
|
<resultMap id="RespMap" type="com.ga.core.vo.commission.CarrierReconciliationInResp"/>
|
||||||
|
|
||||||
|
<!-- 목록/단건 조회 컬럼 — 보험사명 조인 포함 -->
|
||||||
|
<sql id="criJoinCols">
|
||||||
|
cr.recon_id,
|
||||||
|
cr.company_id, ic.company_code, ic.company_name,
|
||||||
|
cr.settle_month,
|
||||||
|
cr.file_name,
|
||||||
|
cr.record_count,
|
||||||
|
cr.reported_total,
|
||||||
|
cr.system_total,
|
||||||
|
cr.variance_amount,
|
||||||
|
cr.status,
|
||||||
|
(SELECT cc.code_name FROM common_code cc
|
||||||
|
WHERE cc.group_code = 'RECON_IN_STATUS' AND cc.code = cr.status) AS status_name,
|
||||||
|
cr.created_at
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<sql id="criJoins">
|
||||||
|
FROM carrier_reconciliation_in cr
|
||||||
|
LEFT JOIN insurance_company ic ON ic.company_id = cr.company_id
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="RespMap">
|
||||||
|
SELECT <include refid="criJoinCols"/>
|
||||||
|
<include refid="criJoins"/>
|
||||||
|
WHERE cr.recon_id = #{reconId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="RespMap">
|
||||||
|
SELECT <include refid="criJoinCols"/>
|
||||||
|
<include refid="criJoins"/>
|
||||||
|
<where>
|
||||||
|
<if test="companyId != null">AND cr.company_id = #{companyId}</if>
|
||||||
|
<if test="settleMonth != null and settleMonth != ''">AND cr.settle_month = #{settleMonth}</if>
|
||||||
|
<if test="status != null and status != ''">AND cr.status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY cr.settle_month DESC, cr.recon_id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="reconId">
|
||||||
|
INSERT INTO carrier_reconciliation_in
|
||||||
|
(company_id, settle_month, file_name, record_count,
|
||||||
|
reported_total, system_total, variance_amount, status,
|
||||||
|
created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(#{companyId}, #{settleMonth}, #{fileName}, COALESCE(#{recordCount}, 0),
|
||||||
|
COALESCE(#{reportedTotal}, 0), COALESCE(#{systemTotal}, 0),
|
||||||
|
COALESCE(#{varianceAmount}, 0),
|
||||||
|
COALESCE(#{status}, 'RECEIVED'), #{createdBy}, NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update">
|
||||||
|
UPDATE carrier_reconciliation_in
|
||||||
|
SET file_name = #{fileName},
|
||||||
|
record_count = #{recordCount},
|
||||||
|
reported_total = #{reportedTotal},
|
||||||
|
system_total = #{systemTotal},
|
||||||
|
variance_amount = #{varianceAmount},
|
||||||
|
status = #{status},
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE recon_id = #{reconId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
보험사 + 정산월 기준 upsert.
|
||||||
|
UNIQUE(company_id, settle_month) 충돌 시 갱신 — 멱등 재처리 지원.
|
||||||
|
-->
|
||||||
|
<insert id="upsert" useGeneratedKeys="true" keyProperty="reconId">
|
||||||
|
INSERT INTO carrier_reconciliation_in
|
||||||
|
(company_id, settle_month, file_name, record_count,
|
||||||
|
reported_total, system_total, variance_amount, status,
|
||||||
|
created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(#{companyId}, #{settleMonth}, #{fileName}, COALESCE(#{recordCount}, 0),
|
||||||
|
COALESCE(#{reportedTotal}, 0), COALESCE(#{systemTotal}, 0),
|
||||||
|
COALESCE(#{varianceAmount}, 0),
|
||||||
|
COALESCE(#{status}, 'RECEIVED'), #{createdBy}, NOW())
|
||||||
|
ON CONFLICT (company_id, settle_month) DO UPDATE
|
||||||
|
SET file_name = EXCLUDED.file_name,
|
||||||
|
record_count = EXCLUDED.record_count,
|
||||||
|
reported_total = EXCLUDED.reported_total,
|
||||||
|
system_total = EXCLUDED.system_total,
|
||||||
|
variance_amount = EXCLUDED.variance_amount,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
updated_at = NOW()
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.core.mapper.commission.ChargebackWaiverRequestMapper">
|
||||||
|
|
||||||
|
<resultMap id="VOMap" type="com.ga.core.vo.commission.ChargebackWaiverRequestVO"/>
|
||||||
|
<resultMap id="RespMap" type="com.ga.core.vo.commission.ChargebackWaiverRequestResp"/>
|
||||||
|
|
||||||
|
<!-- 목록/단건 조회 컬럼 — 설계사명·증권번호·공통코드명 조인 포함 -->
|
||||||
|
<sql id="cwrJoinCols">
|
||||||
|
wr.request_id,
|
||||||
|
wr.contract_id, ct.policy_no,
|
||||||
|
wr.agent_id, a.agent_name, o.org_name,
|
||||||
|
wr.settle_month,
|
||||||
|
wr.original_chargeback,
|
||||||
|
wr.waiver_amount,
|
||||||
|
wr.reason,
|
||||||
|
wr.approval_status,
|
||||||
|
(SELECT cc.code_name FROM common_code cc
|
||||||
|
WHERE cc.group_code = 'WAIVER_STATUS' AND cc.code = wr.approval_status) AS approval_status_name,
|
||||||
|
wr.approver_id,
|
||||||
|
wr.approved_at,
|
||||||
|
wr.created_at
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<sql id="cwrJoins">
|
||||||
|
FROM chargeback_waiver_request wr
|
||||||
|
LEFT JOIN agent a ON a.agent_id = wr.agent_id
|
||||||
|
LEFT JOIN organization o ON o.org_id = a.org_id
|
||||||
|
LEFT JOIN contract ct ON ct.contract_id = wr.contract_id
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="RespMap">
|
||||||
|
SELECT <include refid="cwrJoinCols"/>
|
||||||
|
<include refid="cwrJoins"/>
|
||||||
|
WHERE wr.request_id = #{requestId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="RespMap">
|
||||||
|
SELECT <include refid="cwrJoinCols"/>
|
||||||
|
<include refid="cwrJoins"/>
|
||||||
|
<where>
|
||||||
|
<if test="agentId != null">AND wr.agent_id = #{agentId}</if>
|
||||||
|
<if test="agentName != null and agentName != ''">AND a.agent_name LIKE '%' || #{agentName} || '%'</if>
|
||||||
|
<if test="settleMonth != null and settleMonth != ''">AND wr.settle_month = #{settleMonth}</if>
|
||||||
|
<if test="approvalStatus != null and approvalStatus != ''">AND wr.approval_status = #{approvalStatus}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY wr.settle_month DESC, wr.request_id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- approval_status 기본 'REQUESTED' -->
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="requestId">
|
||||||
|
INSERT INTO chargeback_waiver_request
|
||||||
|
(contract_id, agent_id, settle_month, original_chargeback,
|
||||||
|
waiver_amount, reason, approval_status, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(#{contractId}, #{agentId}, #{settleMonth}, #{originalChargeback},
|
||||||
|
#{waiverAmount}, #{reason}, 'REQUESTED', #{createdBy}, NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 승인/반려 처리 — approval_status, approver_id, approved_at, updated_at 갱신 -->
|
||||||
|
<update id="updateApproval">
|
||||||
|
UPDATE chargeback_waiver_request
|
||||||
|
SET approval_status = #{approvalStatus},
|
||||||
|
approver_id = #{approverId},
|
||||||
|
approved_at = #{approvedAt},
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE request_id = #{requestId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.core.mapper.commission.ClawbackInstallmentPlanMapper">
|
||||||
|
|
||||||
|
<resultMap id="VOMap" type="com.ga.core.vo.commission.ClawbackInstallmentPlanVO"/>
|
||||||
|
<resultMap id="RespMap" type="com.ga.core.vo.commission.ClawbackInstallmentPlanResp"/>
|
||||||
|
|
||||||
|
<!-- 목록/단건 조회 컬럼 — 채권 정보(설계사명·발생월) 조인 포함 -->
|
||||||
|
<sql id="planJoinCols">
|
||||||
|
p.plan_id,
|
||||||
|
p.receivable_id,
|
||||||
|
r.origin_settle_month,
|
||||||
|
r.agent_id, a.agent_name,
|
||||||
|
p.installment_no,
|
||||||
|
p.installment_amount,
|
||||||
|
p.due_month,
|
||||||
|
p.status,
|
||||||
|
(SELECT cc.code_name FROM common_code cc
|
||||||
|
WHERE cc.group_code = 'CB_INSTALLMENT_STATUS' AND cc.code = p.status) AS status_name,
|
||||||
|
p.recovered_at
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<sql id="planJoins">
|
||||||
|
FROM clawback_installment_plan p
|
||||||
|
JOIN clawback_receivable r ON r.receivable_id = p.receivable_id
|
||||||
|
LEFT JOIN agent a ON a.agent_id = r.agent_id
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="RespMap">
|
||||||
|
SELECT <include refid="planJoinCols"/>
|
||||||
|
<include refid="planJoins"/>
|
||||||
|
WHERE p.plan_id = #{planId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="RespMap">
|
||||||
|
SELECT <include refid="planJoinCols"/>
|
||||||
|
<include refid="planJoins"/>
|
||||||
|
<where>
|
||||||
|
<if test="receivableId != null">AND p.receivable_id = #{receivableId}</if>
|
||||||
|
<if test="agentId != null">AND r.agent_id = #{agentId}</if>
|
||||||
|
<if test="dueMonth != null and dueMonth != ''">AND p.due_month = #{dueMonth}</if>
|
||||||
|
<if test="status != null and status != ''">AND p.status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY p.due_month ASC, p.installment_no ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 채권 ID 기준 VO 목록 — 배치/서비스 내부 용도 -->
|
||||||
|
<select id="selectByReceivable" resultMap="VOMap">
|
||||||
|
SELECT plan_id, receivable_id, installment_no, installment_amount,
|
||||||
|
due_month, status, recovered_at,
|
||||||
|
created_at, created_by, updated_at
|
||||||
|
FROM clawback_installment_plan
|
||||||
|
WHERE receivable_id = #{receivableId}
|
||||||
|
ORDER BY installment_no ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
분할상환 계획 일괄 등록.
|
||||||
|
MyBatis foreach — Service에서 List<ClawbackInstallmentPlanVO> 전달.
|
||||||
|
-->
|
||||||
|
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="planId">
|
||||||
|
INSERT INTO clawback_installment_plan
|
||||||
|
(receivable_id, installment_no, installment_amount, due_month,
|
||||||
|
status, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
<foreach collection="list" item="p" separator=",">
|
||||||
|
(#{p.receivableId}, #{p.installmentNo}, #{p.installmentAmount}, #{p.dueMonth},
|
||||||
|
COALESCE(#{p.status}, 'SCHEDULED'), #{p.createdBy}, NOW())
|
||||||
|
</foreach>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 회차 상태 갱신 — RECOVERED 시 recovered_at=NOW() -->
|
||||||
|
<update id="updateStatus">
|
||||||
|
UPDATE clawback_installment_plan
|
||||||
|
SET status = #{status},
|
||||||
|
recovered_at = CASE WHEN #{status} = 'RECOVERED' THEN NOW() ELSE recovered_at END,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE plan_id = #{planId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -194,4 +194,30 @@
|
|||||||
</where>
|
</where>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- [MOD-6] 시효 만료 대상 채권 조회 — beforeMonth = baseMonth - expiration_months -->
|
||||||
|
<select id="selectExpired" resultMap="VOMap">
|
||||||
|
SELECT receivable_id, agent_id, origin_settle_month,
|
||||||
|
original_amount, recovered_amount, balance,
|
||||||
|
status, last_recovery_month,
|
||||||
|
writeoff_reason, written_off_at, written_off_by,
|
||||||
|
created_at, created_by, updated_at
|
||||||
|
FROM clawback_receivable
|
||||||
|
WHERE origin_settle_month <= #{beforeMonth}
|
||||||
|
AND status IN ('OUTSTANDING', 'RECOVERING')
|
||||||
|
ORDER BY origin_settle_month ASC, receivable_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- [MOD-6] 만료 대상 채권 일괄 대손 처리 — WRITTEN_OFF 상태 전이 -->
|
||||||
|
<update id="writeOffExpired">
|
||||||
|
UPDATE clawback_receivable
|
||||||
|
SET balance = 0,
|
||||||
|
status = 'WRITTEN_OFF',
|
||||||
|
writeoff_reason = #{reason},
|
||||||
|
written_off_at = NOW(),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE origin_settle_month <= #{beforeMonth}
|
||||||
|
AND status IN ('OUTSTANDING', 'RECOVERING')
|
||||||
|
</update>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.core.mapper.commission.LapsedReinstatementRecoveryMapper">
|
||||||
|
|
||||||
|
<resultMap id="VOMap" type="com.ga.core.vo.commission.LapsedReinstatementRecoveryVO"/>
|
||||||
|
<resultMap id="RespMap" type="com.ga.core.vo.commission.LapsedReinstatementRecoveryResp"/>
|
||||||
|
|
||||||
|
<!-- 목록/단건 조회 컬럼 — 설계사명·증권번호 조인 포함 -->
|
||||||
|
<sql id="lrrJoinCols">
|
||||||
|
lr.recovery_id,
|
||||||
|
lr.contract_id, ct.policy_no,
|
||||||
|
lr.agent_id, a.agent_name, o.org_name,
|
||||||
|
lr.lapsed_month,
|
||||||
|
lr.reinstated_month,
|
||||||
|
lr.withheld_amount,
|
||||||
|
lr.repaid_amount,
|
||||||
|
lr.status,
|
||||||
|
(SELECT cc.code_name FROM common_code cc
|
||||||
|
WHERE cc.group_code = 'LAPSE_RECOVERY_STATUS' AND cc.code = lr.status) AS status_name
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<sql id="lrrJoins">
|
||||||
|
FROM lapsed_reinstatement_recovery lr
|
||||||
|
LEFT JOIN agent a ON a.agent_id = lr.agent_id
|
||||||
|
LEFT JOIN organization o ON o.org_id = a.org_id
|
||||||
|
LEFT JOIN contract ct ON ct.contract_id = lr.contract_id
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="RespMap">
|
||||||
|
SELECT <include refid="lrrJoinCols"/>
|
||||||
|
<include refid="lrrJoins"/>
|
||||||
|
WHERE lr.recovery_id = #{recoveryId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="RespMap">
|
||||||
|
SELECT <include refid="lrrJoinCols"/>
|
||||||
|
<include refid="lrrJoins"/>
|
||||||
|
<where>
|
||||||
|
<if test="agentId != null">AND lr.agent_id = #{agentId}</if>
|
||||||
|
<if test="agentName != null and agentName != ''">AND a.agent_name LIKE '%' || #{agentName} || '%'</if>
|
||||||
|
<if test="reinstatedMonth != null and reinstatedMonth != ''">AND lr.reinstated_month = #{reinstatedMonth}</if>
|
||||||
|
<if test="lapsedMonth != null and lapsedMonth != ''">AND lr.lapsed_month = #{lapsedMonth}</if>
|
||||||
|
<if test="status != null and status != ''">AND lr.status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY lr.reinstated_month DESC, lr.recovery_id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- repaid_amount 기본 0, status 기본 PENDING -->
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="recoveryId">
|
||||||
|
INSERT INTO lapsed_reinstatement_recovery
|
||||||
|
(contract_id, agent_id, lapsed_month, reinstated_month,
|
||||||
|
withheld_amount, repaid_amount, status, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(#{contractId}, #{agentId}, #{lapsedMonth}, #{reinstatedMonth},
|
||||||
|
#{withheldAmount}, 0, 'PENDING', #{createdBy}, NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 상태 + 재지급액 갱신 — REPAID 시 repaid_amount=withheld_amount(Service에서 전달) -->
|
||||||
|
<update id="updateStatus">
|
||||||
|
UPDATE lapsed_reinstatement_recovery
|
||||||
|
SET status = #{status},
|
||||||
|
repaid_amount = #{repaidAmount},
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE recovery_id = #{recoveryId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<!-- 정산월 기준 설계사별 재지급 합계 — status='REPAID' AND reinstated_month=settleMonth -->
|
||||||
|
<select id="aggregateByAgent" resultType="map">
|
||||||
|
SELECT agent_id AS "agentId",
|
||||||
|
SUM(repaid_amount) AS "total"
|
||||||
|
FROM lapsed_reinstatement_recovery
|
||||||
|
WHERE reinstated_month = #{settleMonth}
|
||||||
|
AND status = 'REPAID'
|
||||||
|
GROUP BY agent_id
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.core.mapper.commission.ReconciliationVarianceMapper">
|
||||||
|
|
||||||
|
<resultMap id="VOMap" type="com.ga.core.vo.commission.ReconciliationVarianceVO"/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
차이 상세 일괄 등록.
|
||||||
|
MyBatis foreach — Service에서 List<ReconciliationVarianceVO> 전달.
|
||||||
|
-->
|
||||||
|
<insert id="insertBatch">
|
||||||
|
INSERT INTO reconciliation_variance
|
||||||
|
(recon_id, policy_no, expected_amount, reported_amount,
|
||||||
|
variance_reason, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
<foreach collection="list" item="v" separator=",">
|
||||||
|
(#{v.reconId}, #{v.policyNo}, #{v.expectedAmount}, #{v.reportedAmount},
|
||||||
|
#{v.varianceReason}, #{v.createdBy}, NOW())
|
||||||
|
</foreach>
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<!-- 대사 입수 ID 기준 차이 상세 목록 조회 -->
|
||||||
|
<select id="selectByRecon" resultMap="VOMap">
|
||||||
|
SELECT variance_id, recon_id, policy_no,
|
||||||
|
expected_amount, reported_amount, variance_reason,
|
||||||
|
created_at, created_by
|
||||||
|
FROM reconciliation_variance
|
||||||
|
WHERE recon_id = #{reconId}
|
||||||
|
ORDER BY variance_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.core.mapper.commission.RecruiterCommissionLedgerMapper">
|
||||||
|
|
||||||
|
<resultMap id="VOMap" type="com.ga.core.vo.commission.RecruiterCommissionLedgerVO"/>
|
||||||
|
<resultMap id="RespMap" type="com.ga.core.vo.commission.RecruiterCommissionLedgerResp"/>
|
||||||
|
|
||||||
|
<!-- 목록/단건 조회 컬럼 — 도입자·피도입 설계사명·공통코드명 조인 포함 -->
|
||||||
|
<sql id="ledgerJoinCols">
|
||||||
|
l.ledger_id,
|
||||||
|
l.recruiter_agent_id,
|
||||||
|
ra.agent_name AS recruiter_agent_name,
|
||||||
|
l.recruit_agent_id,
|
||||||
|
na.agent_name AS recruit_agent_name,
|
||||||
|
l.settle_month,
|
||||||
|
l.recruiter_type,
|
||||||
|
(SELECT cc.code_name FROM common_code cc
|
||||||
|
WHERE cc.group_code = 'RECRUITER_TYPE' AND cc.code = l.recruiter_type) AS recruiter_type_name,
|
||||||
|
l.base_amount,
|
||||||
|
l.commission_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
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<sql id="ledgerJoins">
|
||||||
|
FROM recruiter_commission_ledger l
|
||||||
|
LEFT JOIN agent ra ON ra.agent_id = l.recruiter_agent_id
|
||||||
|
LEFT JOIN agent na ON na.agent_id = l.recruit_agent_id
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="RespMap">
|
||||||
|
SELECT <include refid="ledgerJoinCols"/>
|
||||||
|
<include refid="ledgerJoins"/>
|
||||||
|
WHERE l.ledger_id = #{ledgerId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="RespMap">
|
||||||
|
SELECT <include refid="ledgerJoinCols"/>
|
||||||
|
<include refid="ledgerJoins"/>
|
||||||
|
<where>
|
||||||
|
<if test="recruiterAgentId != null">AND l.recruiter_agent_id = #{recruiterAgentId}</if>
|
||||||
|
<if test="recruitAgentId != null">AND l.recruit_agent_id = #{recruitAgentId}</if>
|
||||||
|
<if test="settleMonth != null and settleMonth != ''">AND l.settle_month = #{settleMonth}</if>
|
||||||
|
<if test="recruiterType != null and recruiterType != ''">AND l.recruiter_type = #{recruiterType}</if>
|
||||||
|
<if test="status != null and status != ''">AND l.status = #{status}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY l.settle_month DESC, l.recruiter_agent_id, l.ledger_id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- status 기본값 'CALCULATED' -->
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="ledgerId">
|
||||||
|
INSERT INTO recruiter_commission_ledger
|
||||||
|
(recruiter_agent_id, recruit_agent_id, settle_month, recruiter_type,
|
||||||
|
base_amount, commission_amount, status, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(#{recruiterAgentId}, #{recruitAgentId}, #{settleMonth}, #{recruiterType},
|
||||||
|
#{baseAmount}, #{commissionAmount},
|
||||||
|
COALESCE(#{status}, 'CALCULATED'), #{createdBy}, NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updateStatus">
|
||||||
|
UPDATE recruiter_commission_ledger
|
||||||
|
SET status = #{status}, updated_at = NOW()
|
||||||
|
WHERE ledger_id = #{ledgerId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="delete">
|
||||||
|
DELETE FROM recruiter_commission_ledger WHERE ledger_id = #{ledgerId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<!-- 정산월 기준 도입자 설계사별 도입수수료 합계 — CANCELLED 제외, agentId alias -->
|
||||||
|
<select id="aggregateByAgent" resultType="map">
|
||||||
|
SELECT recruiter_agent_id AS "agentId",
|
||||||
|
SUM(commission_amount) AS "total"
|
||||||
|
FROM recruiter_commission_ledger
|
||||||
|
WHERE settle_month = #{settleMonth}
|
||||||
|
AND status != 'CANCELLED'
|
||||||
|
GROUP BY recruiter_agent_id
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.core.mapper.commission.RecruiterCommissionRuleMapper">
|
||||||
|
|
||||||
|
<resultMap id="VOMap" type="com.ga.core.vo.commission.RecruiterCommissionRuleVO"/>
|
||||||
|
<resultMap id="RespMap" type="com.ga.core.vo.commission.RecruiterCommissionRuleResp"/>
|
||||||
|
|
||||||
|
<!-- 목록/단건 조회 컬럼 — 공통코드명 포함 -->
|
||||||
|
<sql id="ruleCols">
|
||||||
|
r.rule_id,
|
||||||
|
r.recruiter_type,
|
||||||
|
(SELECT cc.code_name FROM common_code cc
|
||||||
|
WHERE cc.group_code = 'RECRUITER_TYPE' AND cc.code = r.recruiter_type) AS recruiter_type_name,
|
||||||
|
r.calc_type,
|
||||||
|
(SELECT cc.code_name FROM common_code cc
|
||||||
|
WHERE cc.group_code = 'CALC_TYPE' AND cc.code = r.calc_type) AS calc_type_name,
|
||||||
|
r.commission_rate,
|
||||||
|
r.fixed_amount,
|
||||||
|
r.effective_from,
|
||||||
|
r.effective_to
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="RespMap">
|
||||||
|
SELECT <include refid="ruleCols"/>
|
||||||
|
FROM recruiter_commission_rule r
|
||||||
|
WHERE r.rule_id = #{ruleId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="RespMap">
|
||||||
|
SELECT <include refid="ruleCols"/>
|
||||||
|
FROM recruiter_commission_rule r
|
||||||
|
<where>
|
||||||
|
<if test="recruiterType != null and recruiterType != ''">AND r.recruiter_type = #{recruiterType}</if>
|
||||||
|
<if test="calcType != null and calcType != ''">AND r.calc_type = #{calcType}</if>
|
||||||
|
</where>
|
||||||
|
ORDER BY r.effective_from DESC, r.rule_id DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="ruleId">
|
||||||
|
INSERT INTO recruiter_commission_rule
|
||||||
|
(recruiter_type, calc_type, commission_rate, fixed_amount,
|
||||||
|
effective_from, effective_to, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(#{recruiterType}, #{calcType}, #{commissionRate}, #{fixedAmount},
|
||||||
|
#{effectiveFrom}, #{effectiveTo}, #{createdBy}, NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update">
|
||||||
|
UPDATE recruiter_commission_rule
|
||||||
|
SET recruiter_type = #{recruiterType},
|
||||||
|
calc_type = #{calcType},
|
||||||
|
commission_rate = #{commissionRate},
|
||||||
|
fixed_amount = #{fixedAmount},
|
||||||
|
effective_from = #{effectiveFrom},
|
||||||
|
effective_to = #{effectiveTo},
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE rule_id = #{ruleId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="delete">
|
||||||
|
DELETE FROM recruiter_commission_rule WHERE rule_id = #{ruleId}
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.ga.core.mapper.commission.StatuteConfigMapper">
|
||||||
|
|
||||||
|
<resultMap id="VOMap" type="com.ga.core.vo.commission.StatuteConfigVO"/>
|
||||||
|
<resultMap id="RespMap" type="com.ga.core.vo.commission.StatuteConfigResp"/>
|
||||||
|
|
||||||
|
<select id="selectById" resultMap="RespMap">
|
||||||
|
SELECT config_id, entity_type, expiration_months, is_active
|
||||||
|
FROM statute_config
|
||||||
|
WHERE config_id = #{configId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- entity_type UNIQUE — 단건 반환 -->
|
||||||
|
<select id="selectByEntityType" resultMap="RespMap">
|
||||||
|
SELECT config_id, entity_type, expiration_months, is_active
|
||||||
|
FROM statute_config
|
||||||
|
WHERE entity_type = #{entityType}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectList" resultMap="RespMap">
|
||||||
|
SELECT config_id, entity_type, expiration_months, is_active
|
||||||
|
FROM statute_config
|
||||||
|
ORDER BY config_id ASC
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insert" useGeneratedKeys="true" keyProperty="configId">
|
||||||
|
INSERT INTO statute_config
|
||||||
|
(entity_type, expiration_months, is_active, created_by, created_at)
|
||||||
|
VALUES
|
||||||
|
(#{entityType}, #{expirationMonths}, COALESCE(#{isActive}, 'Y'),
|
||||||
|
#{createdBy}, NOW())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="update">
|
||||||
|
UPDATE statute_config
|
||||||
|
SET expiration_months = #{expirationMonths},
|
||||||
|
is_active = #{isActive},
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE config_id = #{configId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -18,7 +18,10 @@
|
|||||||
ct.status,
|
ct.status,
|
||||||
(SELECT cc.code_name FROM common_code cc
|
(SELECT cc.code_name FROM common_code cc
|
||||||
WHERE cc.group_code='CONTRACT_STATUS' AND cc.code=ct.status) AS status_name,
|
WHERE cc.group_code='CONTRACT_STATUS' AND cc.code=ct.status) AS status_name,
|
||||||
ct.contract_date, ct.effective_date, ct.lapse_date
|
ct.contract_date, ct.effective_date, ct.lapse_date,
|
||||||
|
ct.sales_channel,
|
||||||
|
(SELECT cc.code_name FROM common_code cc
|
||||||
|
WHERE cc.group_code='SALES_CHANNEL' AND cc.code=ct.sales_channel) AS sales_channel_name
|
||||||
</sql>
|
</sql>
|
||||||
|
|
||||||
<select id="selectList" resultMap="ContractRespMap">
|
<select id="selectList" resultMap="ContractRespMap">
|
||||||
@@ -84,11 +87,11 @@
|
|||||||
<insert id="insert" parameterType="com.ga.core.vo.product.ContractVO" useGeneratedKeys="true" keyProperty="contractId">
|
<insert id="insert" parameterType="com.ga.core.vo.product.ContractVO" useGeneratedKeys="true" keyProperty="contractId">
|
||||||
INSERT INTO contract (agent_id, product_id, policy_no, contractor_name, insured_name,
|
INSERT INTO contract (agent_id, product_id, policy_no, contractor_name, insured_name,
|
||||||
premium, pay_cycle, pay_period, coverage_period, status,
|
premium, pay_cycle, pay_period, coverage_period, status,
|
||||||
contract_date, effective_date, created_at)
|
contract_date, effective_date, sales_channel, created_at)
|
||||||
VALUES (#{agentId}, #{productId}, #{policyNo}, #{contractorName}, #{insuredName},
|
VALUES (#{agentId}, #{productId}, #{policyNo}, #{contractorName}, #{insuredName},
|
||||||
#{premium}, #{payCycle}, #{payPeriod}, #{coveragePeriod},
|
#{premium}, #{payCycle}, #{payPeriod}, #{coveragePeriod},
|
||||||
COALESCE(#{status},'ACTIVE'),
|
COALESCE(#{status},'ACTIVE'),
|
||||||
#{contractDate}, #{effectiveDate}, #{createdAt})
|
#{contractDate}, #{effectiveDate}, #{salesChannel}, #{createdAt})
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<update id="update" parameterType="com.ga.core.vo.product.ContractVO">
|
<update id="update" parameterType="com.ga.core.vo.product.ContractVO">
|
||||||
@@ -105,6 +108,7 @@
|
|||||||
effective_date = #{effectiveDate},
|
effective_date = #{effectiveDate},
|
||||||
lapse_date = #{lapseDate},
|
lapse_date = #{lapseDate},
|
||||||
revival_date = #{revivalDate},
|
revival_date = #{revivalDate},
|
||||||
|
sales_channel = #{salesChannel},
|
||||||
updated_at = #{updatedAt}
|
updated_at = #{updatedAt}
|
||||||
WHERE contract_id = #{contractId}
|
WHERE contract_id = #{contractId}
|
||||||
</update>
|
</update>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user