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
|
||||
token.txt
|
||||
__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.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Transactional(readOnly = true)
|
||||
@@ -86,6 +89,24 @@ public class PersistencyBonusService {
|
||||
*/
|
||||
@Transactional
|
||||
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();
|
||||
vo.setAgentId(req.getAgentId());
|
||||
vo.setRuleId(req.getRuleId());
|
||||
@@ -93,7 +114,7 @@ public class PersistencyBonusService {
|
||||
vo.setRetentionMonth(req.getRetentionMonth());
|
||||
vo.setSettleMonth(req.getSettleMonth());
|
||||
vo.setRetentionRate(req.getRetentionRate());
|
||||
vo.setBonusAmount(req.getBonusAmount());
|
||||
vo.setBonusAmount(bonusAmount);
|
||||
vo.setStatus("CALCULATED");
|
||||
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;
|
||||
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) {
|
||||
log.warn("[수수료율 누락] company_rate 없음 → 0 폴백(과소지급 위험): contractId={}, productId={}, year={}, baseDate={}",
|
||||
c.getContractId(), c.getProductId(), year, baseDate);
|
||||
@@ -58,7 +58,7 @@ public class CommissionCalculator {
|
||||
}
|
||||
|
||||
BigDecimal payoutRate = ruleMapper.findPayoutRate(
|
||||
agent.getGradeId(), c.getInsuranceType(), year, baseDate);
|
||||
agent.getGradeId(), c.getInsuranceType(), year, baseDate, c.getSalesChannel());
|
||||
if (payoutRate == null) {
|
||||
log.warn("[수수료율 누락] payout_rate 없음 → 0 폴백(과소지급 위험): contractId={}, gradeId={}, insuranceType={}, year={}, baseDate={}",
|
||||
c.getContractId(), agent.getGradeId(), c.getInsuranceType(), year, baseDate);
|
||||
|
||||
+7
-2
@@ -40,6 +40,9 @@ public class BatchInstallmentPlanGenerator {
|
||||
List<InstallmentPlanVO> plans = new ArrayList<>();
|
||||
for (int i = 0; i < ratios.size(); i++) {
|
||||
InstallmentRatioVO ratio = ratios.get(i);
|
||||
// 마스터의 plan_month 가 NULL 이면 연 단위(첫 달 지급), NOT NULL 이면 그 회차(월) 그대로 사용.
|
||||
int planMonth = ratio.getPlanMonth() != null ? ratio.getPlanMonth() : 1;
|
||||
|
||||
BigDecimal amount;
|
||||
if (fullSchedule && i == ratios.size() - 1) {
|
||||
amount = totalCommission.subtract(allocated);
|
||||
@@ -52,8 +55,10 @@ public class BatchInstallmentPlanGenerator {
|
||||
InstallmentPlanVO plan = new InstallmentPlanVO();
|
||||
plan.setContractId(contractId);
|
||||
plan.setPlanYear(ratio.getPlanYear());
|
||||
plan.setPlanMonth(1);
|
||||
plan.setSettleMonth(contractDate.plusYears(ratio.getPlanYear()).format(MONTH_FMT));
|
||||
plan.setPlanMonth(planMonth);
|
||||
plan.setSettleMonth(
|
||||
contractDate.plusYears(ratio.getPlanYear()).plusMonths(planMonth - 1L).format(MONTH_FMT)
|
||||
);
|
||||
plan.setExpectedAmount(amount);
|
||||
plan.setStatus(InstallmentStatus.SCHEDULED.code());
|
||||
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.TaxBreakdown;
|
||||
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.BranchMgrAllowanceLedgerMapper;
|
||||
import com.ga.core.mapper.commission.ClawbackReceivableMapper;
|
||||
import com.ga.core.mapper.commission.ClawbackRecoveryMapper;
|
||||
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.PersistencyBonusMapper;
|
||||
import com.ga.core.mapper.commission.RecruiterCommissionLedgerMapper;
|
||||
import com.ga.core.mapper.commission.ReferralCommissionLedgerMapper;
|
||||
import com.ga.core.mapper.commission.ReinstatementCommissionLedgerMapper;
|
||||
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.OverrideSettleMapper;
|
||||
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.ClawbackReceivableVO;
|
||||
import com.ga.core.vo.commission.ClawbackRecoveryVO;
|
||||
@@ -96,6 +102,14 @@ public class AggregateStep implements Tasklet {
|
||||
private final LifeSupportLedgerMapper lifeSupportMapper;
|
||||
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
|
||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
||||
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> 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
|
||||
Set<Long> agentIds = new HashSet<>();
|
||||
agentIds.addAll(recruitMap.keySet());
|
||||
@@ -142,6 +161,8 @@ public class AggregateStep implements Tasklet {
|
||||
agentIds.addAll(awardMap.keySet());
|
||||
agentIds.addAll(lifeSupportMap.keySet());
|
||||
agentIds.addAll(branchMgrMap.keySet());
|
||||
agentIds.addAll(recruiterCommMap.keySet());
|
||||
agentIds.addAll(lapsedReinstatMap.keySet());
|
||||
|
||||
// 활성 설계사 누락 보충
|
||||
AgentSearchParam ap = new AgentSearchParam();
|
||||
@@ -168,6 +189,9 @@ public class AggregateStep implements Tasklet {
|
||||
// 동일 월 재실행 시 중복 회수가 발생하지 않도록 보장한다.
|
||||
reverseClawbackRecovery(ctx.getSettleMonth());
|
||||
|
||||
// MOD-2 멱등 reverse: 선지급 상계 이력 역산 후 advance_payment balance 복원
|
||||
reverseAdvanceRecovery(ctx.getSettleMonth());
|
||||
|
||||
for (Long agentId : agentIds) {
|
||||
BigDecimal recruit = recruitMap.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, lifeSupportMap.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)
|
||||
.subtract(cb).subtract(em);
|
||||
@@ -230,6 +256,17 @@ public class AggregateStep implements Tasklet {
|
||||
log.info("[Step8 clawback] agentId={}, net={}, 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
|
||||
SettleMasterVO vo = new SettleMasterVO();
|
||||
vo.setAgentId(agentId);
|
||||
@@ -312,6 +349,74 @@ public class AggregateStep implements Tasklet {
|
||||
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 이력을 조회하여
|
||||
* 각 receivable의 balance/recoveredAmount/status를 복원한 뒤 회수 이력을 전량 삭제한다.
|
||||
|
||||
@@ -75,32 +75,43 @@ public class CalcRecruitStep implements Tasklet {
|
||||
LedgerVO led = calculator.calcRecruitLedger(c, ctx.getSettleMonth());
|
||||
if (led == null || MoneyUtil.isZero(led.getAgentAmount())) continue;
|
||||
|
||||
// 1200%룰 검증 (1차년도 한도)
|
||||
// 1200%룰 검증: 1차년 한도(FIRST_YEAR) + 누적 한도(TOTAL 1200%) 중
|
||||
// 더 제한적인 한도로 당월 지급액을 clip, 초과분은 분급(7년)으로 이연.
|
||||
if (c.getContractDate() != null) {
|
||||
BigDecimal originalAmount = led.getAgentAmount();
|
||||
BigDecimal allowed = originalAmount;
|
||||
|
||||
ViolationResult firstYearCheck = regulatoryChecker.checkFirstYear(
|
||||
c.getContractId(), led.getAgentAmount(), c.getContractDate());
|
||||
|
||||
c.getContractId(), originalAmount, c.getContractDate());
|
||||
if (firstYearCheck.violated()) {
|
||||
violationCount++;
|
||||
|
||||
// 1차년 한도 잔여분까지만 당월 지급, 초과분은 분급(7년)으로 이연.
|
||||
BigDecimal allowed = MoneyUtil.sub(
|
||||
BigDecimal fyAllowed = MoneyUtil.sub(
|
||||
firstYearCheck.limitAmount(), firstYearCheck.currentTotal());
|
||||
if (allowed.signum() < 0) allowed = BigDecimal.ZERO;
|
||||
BigDecimal excess = MoneyUtil.sub(led.getAgentAmount(), allowed);
|
||||
if (fyAllowed.signum() < 0) fyAllowed = BigDecimal.ZERO;
|
||||
allowed = allowed.min(fyAllowed);
|
||||
}
|
||||
|
||||
if (excess.signum() > 0) {
|
||||
// 초과분만 분급 계획 생성 (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%룰 1차년 한도초과 → clip+이연: contractId={}, 지급(한도잔여)={}, 이연(분급)={}",
|
||||
c.getContractId(), allowed, excess);
|
||||
}
|
||||
ViolationResult totalCheck = regulatoryChecker.checkContractTotal(
|
||||
c.getContractId(), originalAmount, c.getContractDate());
|
||||
if (totalCheck.violated()) {
|
||||
BigDecimal totAllowed = MoneyUtil.sub(
|
||||
totalCheck.limitAmount(), totalCheck.currentTotal());
|
||||
if (totAllowed.signum() < 0) totAllowed = BigDecimal.ZERO;
|
||||
allowed = allowed.min(totAllowed);
|
||||
}
|
||||
|
||||
BigDecimal excess = MoneyUtil.sub(originalAmount, allowed);
|
||||
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
|
||||
@DisplayName("calcRecruitLedger: premium × company_rate × payout_rate, 세금은 3.3%")
|
||||
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"));
|
||||
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"));
|
||||
|
||||
LedgerVO led = calculator.calcRecruitLedger(contract, "202405");
|
||||
@@ -83,12 +83,30 @@ class CommissionCalculatorTest {
|
||||
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
|
||||
@DisplayName("calcMaintainLedger: 회차에 따라 commission_year 가 다름")
|
||||
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"));
|
||||
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"));
|
||||
|
||||
LedgerVO led = calculator.calcMaintainLedger(contract, "202405", 3);
|
||||
@@ -115,8 +133,8 @@ class CommissionCalculatorTest {
|
||||
@Test
|
||||
@DisplayName("규정 누락 시 0 으로 폴백 → 모든 금액 0")
|
||||
void missingRates_fallbackZero() {
|
||||
when(ruleMapper.findCompanyRate(any(), any(), any())).thenReturn(null);
|
||||
when(ruleMapper.findPayoutRate(any(), any(), any(), any())).thenReturn(null);
|
||||
when(ruleMapper.findCompanyRate(any(), any(), any(), any())).thenReturn(null);
|
||||
when(ruleMapper.findPayoutRate(any(), any(), any(), any(), any())).thenReturn(null);
|
||||
|
||||
LedgerVO led = calculator.calcRecruitLedger(contract, "202405");
|
||||
|
||||
@@ -132,9 +150,9 @@ class CommissionCalculatorTest {
|
||||
@DisplayName("contractDate 가 null 이면 오늘 날짜로 대체 (예외 없음)")
|
||||
void nullContractDate_usesToday() {
|
||||
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"));
|
||||
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"));
|
||||
|
||||
LedgerVO led = calculator.calcRecruitLedger(contract, "202405");
|
||||
@@ -148,8 +166,8 @@ class CommissionCalculatorTest {
|
||||
@DisplayName("premium 이 null 이면 0 으로 처리 (HALF_UP 0.00)")
|
||||
void nullPremium() {
|
||||
contract.setPremium(null);
|
||||
when(ruleMapper.findCompanyRate(any(), any(), any())).thenReturn(new BigDecimal("7.5"));
|
||||
when(ruleMapper.findPayoutRate(any(), any(), any(), any())).thenReturn(new BigDecimal("60"));
|
||||
when(ruleMapper.findCompanyRate(any(), any(), any(), any())).thenReturn(new BigDecimal("7.5"));
|
||||
when(ruleMapper.findPayoutRate(any(), any(), any(), any(), any())).thenReturn(new BigDecimal("60"));
|
||||
|
||||
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
|
||||
@DisplayName("① 보험 인입 → 모집수수료 원장: 1,000,000 ×60% ×75% = 450,000, 세금 14,850")
|
||||
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"));
|
||||
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"));
|
||||
|
||||
LedgerVO led = commissionCalculator.calcRecruitLedger(contract, "202401");
|
||||
@@ -226,9 +226,9 @@ class SettlementScenarioTest {
|
||||
@Test
|
||||
@DisplayName("④ 유지수수료 2차년: 1,000,000 ×10% ×50% = 50,000")
|
||||
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"));
|
||||
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"));
|
||||
|
||||
LedgerVO led = commissionCalculator.calcMaintainLedger(contract, "202501", 2);
|
||||
@@ -241,10 +241,10 @@ class SettlementScenarioTest {
|
||||
@Test
|
||||
@DisplayName("⑤ 계약 승계: 유지수수료는 후임(박승계 등급3 40%)에게 귀속, 모집은 원설계사 유지")
|
||||
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"));
|
||||
// 후임 박승계 등급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"));
|
||||
|
||||
LedgerVO led = commissionCalculator.calcMaintainLedger(contract, "202501", 2, PARK);
|
||||
|
||||
@@ -57,6 +57,30 @@ class CalcRecruitStepTest {
|
||||
step = new CalcRecruitStep(ctx, contractMapper, recruitMapper,
|
||||
calculator, regulatoryChecker, installmentGenerator, installmentPlanMapper);
|
||||
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
|
||||
@@ -100,4 +124,62 @@ class CalcRecruitStepTest {
|
||||
assertThat(saved.getAgentAmount()).isEqualByComparingTo("350000");
|
||||
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);
|
||||
|
||||
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 ===
|
||||
List<CommissionRateVO> selectCommissionRates(@Param("productId") Long productId,
|
||||
@Param("year") Integer year);
|
||||
/** 계약일에 유효한 보험사 수수료율 */
|
||||
/** 계약일에 유효한 보험사 수수료율 (판매채널 우선, NULL=모든채널 폴백) */
|
||||
BigDecimal findCompanyRate(@Param("productId") Long productId,
|
||||
@Param("year") Integer year,
|
||||
@Param("baseDate") LocalDate baseDate);
|
||||
@Param("baseDate") LocalDate baseDate,
|
||||
@Param("salesChannel") String salesChannel);
|
||||
int insertCommissionRate(CommissionRateVO vo);
|
||||
int updateCommissionRate(CommissionRateVO vo);
|
||||
int deleteCommissionRate(@Param("rateId") Long rateId);
|
||||
@@ -31,7 +32,8 @@ public interface RuleMapper {
|
||||
BigDecimal findPayoutRate(@Param("gradeId") Long gradeId,
|
||||
@Param("insuranceType") String insuranceType,
|
||||
@Param("year") Integer year,
|
||||
@Param("baseDate") LocalDate baseDate);
|
||||
@Param("baseDate") LocalDate baseDate,
|
||||
@Param("salesChannel") String salesChannel);
|
||||
int insertPayoutRule(PayoutRuleVO vo);
|
||||
int updatePayoutRule(PayoutRuleVO vo);
|
||||
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 effectiveDate;
|
||||
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;
|
||||
@NotNull private LocalDate contractDate;
|
||||
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 lapseDate;
|
||||
private LocalDate revivalDate;
|
||||
/** 판매채널 (NULL=DIRECT 취급): DIRECT/BANCASSURANCE/HOME_SHOPPING/PARTNERSHIP (V123) */
|
||||
private String salesChannel;
|
||||
private LocalDateTime createdAt;
|
||||
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>
|
||||
</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>
|
||||
|
||||
@@ -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,
|
||||
(SELECT cc.code_name FROM common_code cc
|
||||
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>
|
||||
|
||||
<select id="selectList" resultMap="ContractRespMap">
|
||||
@@ -84,11 +87,11 @@
|
||||
<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,
|
||||
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},
|
||||
#{premium}, #{payCycle}, #{payPeriod}, #{coveragePeriod},
|
||||
COALESCE(#{status},'ACTIVE'),
|
||||
#{contractDate}, #{effectiveDate}, #{createdAt})
|
||||
#{contractDate}, #{effectiveDate}, #{salesChannel}, #{createdAt})
|
||||
</insert>
|
||||
|
||||
<update id="update" parameterType="com.ga.core.vo.product.ContractVO">
|
||||
@@ -105,6 +108,7 @@
|
||||
effective_date = #{effectiveDate},
|
||||
lapse_date = #{lapseDate},
|
||||
revival_date = #{revivalDate},
|
||||
sales_channel = #{salesChannel},
|
||||
updated_at = #{updatedAt}
|
||||
WHERE contract_id = #{contractId}
|
||||
</update>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user