feat: incar 갭 반영 수수료/운영 14개 도메인 풀스택 + 배치통합 + 신입교육자료

incar CMS(116메뉴) 벤치마킹 갭분석으로 ga-pro에 없던 14개 도메인을 풀스택 추가.

P7 수수료 계산 7종 (V88~V96):
- 수입수수료+수지차, 소개·이관수수료, 정착지원금, 인정실적+환산율,
  시상, 생보운영지원수수료, 지점장수당
- 각 rule/ledger + 공식(보험료×요율, 수입−지급 등) Service 내장

P8 운영/정산 7종 (V97~V105):
- 단계마감, 본사정산, 등급평가, 공동계약, 보증보험, 적립금, 수수료시뮬레이터
- close/reopen/apply/simulate/upsert 등 액션 + 손익·잔액·등급 산정 로직

배치 통합 (V106):
- settle_master.other_commission_total 컬럼 추가(DEFAULT 0, 백필)
- 설계사 지급성 9종 원장 aggregateByAgent → AggregateStep gross 합산
- 빈 원장 0 → 기존 정산결과·무결성 불변(비파괴적)

테스트: P7/P8 Service 공식 단위테스트 21건(ga-api:test 57건 전체 통과)
프론트: 화면 14개 + React.lazy 코드 스플리팅(단일 4MB → 130+ 청크)
문서: DOMAIN_GAP_P7/P8.md, 신입교육_보험과수수료_완전기초.md, DOMAIN_KNOWLEDGE/HANDOFF 갱신

검증: 전체 ./gradlew build(test 포함) SUCCESSFUL, Flyway V88~V106 success,
GET+액션POST smoke 5xx 0건, 9개 집계쿼리 SQL 유효성 확인, 공식 런타임 실측.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-05-31 15:45:10 +09:00
parent 70620463b4
commit bfc724669b
231 changed files with 13325 additions and 189 deletions
+24 -3
View File
@@ -91,6 +91,13 @@
| **유지보수당(지속수당)** | persistency bonus | 일정 유지율 달성 시 | 설계사 | 보너스 성격 | | **유지보수당(지속수당)** | persistency bonus | 일정 유지율 달성 시 | 설계사 | 보너스 성격 |
| **오버라이드** | override | 하위 설계사 실적 발생 시 | **상위 직책자** | 조직 트리 기반 수당 | | **오버라이드** | override | 하위 설계사 실적 발생 시 | **상위 직책자** | 조직 트리 기반 수당 |
| **시책·인센티브** | incentive | 캠페인/목표 달성 시 | 설계사 | 한시적 프로모션 | | **시책·인센티브** | incentive | 캠페인/목표 달성 시 | 설계사 | 한시적 프로모션 |
| **수입수수료** | income | 보험사가 GA에 지급한 수수료 수입 원장 | GA 경영진/재무 | 수지차(=수입-지급) 집계용. 설계사 지급과 분리 관리 |
| **소개·이관수수료** | referral/transfer | 계약을 소개(REFERRAL)하거나 이관(TRANSFER)한 설계사에게 | 소개/이관 설계사 | 정률 또는 정액. 기준금액×율 또는 고정액 |
| **정착지원금** | settling support | 신입/경력 설계사 입사 후 약정 기간 | 신규 설계사 | 등급별 월 정액. 조기 해촉 시 잔여약정 비율로 환수 |
| **인정실적** | recognized performance | 계약 체결 시 (환산보험료 기준) | 설계사 | 환산보험료×인정율. 시상·등급 산정의 기초 지표 |
| **시상** | award | 목표 실적 달성 시 | 설계사 | 신인도입/법인/유지율/실적 시상. 정률 또는 정액 |
| **생보운영지원수수료** | life op support | 생보 계약 운영 실적 기준 | 설계사 | 실적×지급률. 환수 가능 |
| **지점장수당** | branch mgr allowance | 소속 조직 실적 발생 시 | 지점장 | 조직실적×수당율, cap(상한) 제한. 환수 가능 |
> **모집 vs 유지의 귀속 차이 (중요):** 설계사가 해촉·이관되면, 그 계약의 **유지수수료는 후임 설계사**에게 넘어가지만(계약 승계), > **모집 vs 유지의 귀속 차이 (중요):** 설계사가 해촉·이관되면, 그 계약의 **유지수수료는 후임 설계사**에게 넘어가지만(계약 승계),
> **모집수수료는 원래 모집한 설계사**에게 그대로 남는다. (한국 고아계약 승계 표준) > **모집수수료는 원래 모집한 설계사**에게 그대로 남는다. (한국 고아계약 승계 표준)
@@ -245,7 +252,21 @@ gross(총수수료)
> 여기에 가지급금 등 **공제**가 있으면 net에서 추가로 빠집니다. > 여기에 가지급금 등 **공제**가 있으면 net에서 추가로 빠집니다.
> 비과세/면세 설계사는 부가세(+45,000) 항목이 없습니다. > 비과세/면세 설계사는 부가세(+45,000) 항목이 없습니다.
### 7-7. 한 장 정리 ### 7-7. P7 신규 수수료 계산 공식 (V88~V96, incar 갭 반영)
| 종류 | 공식 | 비고 |
|---|---|---|
| **수입수수료** | `income_amount = premium × company_rate` | 보험사→GA 수입 원장. 수지차 = Σincome Σpayout |
| **소개·이관수수료** | `commission_amount = base × rate` (RATE형) 또는 `fixed_amount` (FIXED형) | 계약 소개/이관 설계사에게 지급 |
| **정착지원금** | 지급: 등급별 `support_amount` (월 정액). 환수: `support_amount × (잔여약정개월 / 약정개월)` (조기 해촉 시) | `net = support_amount clawback_amount` |
| **인정실적(환산보험료)** | `converted_premium = premium × conversion_rate`. `recognized_amount = converted_premium × recognition_rate` | 시상/등급 산정 기초. 상품별 환산율은 `product_conversion_rate` |
| **시상** | `award_amount = base × award_rate` (RATE형) 또는 `award_fixed` (FIXED형) | `threshold_amount` 이상 실적 달성 시만 발동 |
| **생보운영지원수수료** | `support_amount = base × support_rate`. `net = support_amount clawback_amount` | 생보 실적 기준 지급 |
| **지점장수당** | `allowance_amount = LEAST(base × allowance_rate, cap_amount)` | cap=NULL이면 무제한. `net = allowance_amount clawback_amount` |
> 모든 금액 `NUMERIC(15,2)`, 율 `NUMERIC(7,4)`(0~1 소수 표현), 반올림 HALF_UP.
### 7-8. 한 장 정리
``` ```
보험료 ──×보험사요율──▶ 보험사지급액 ──×지급률──▶ 설계사수수료(gross) 보험료 ──×보험사요율──▶ 보험사지급액 ──×지급률──▶ 설계사수수료(gross)
@@ -359,5 +380,5 @@ DRAFT(계산됨) → CONFIRMED(확정) → PAID(지급완료)
--- ---
*기준: 한국 보험업감독규정 표준 가정값. 시스템 schema v87 (2026-05 기준). 표준값은 모두 데이터로 외부화되어 코드 변경 없이 조정 가능.* *기준: 한국 보험업감독규정 표준 가정값. 시스템 schema v96 (2026-05-31 기준, P7 incar 갭 반영). 표준값은 모두 데이터로 외부화되어 코드 변경 없이 조정 가능.*
*계산 공식 출처: `CommissionCalculator` / `TaxCalculator` / `CalcOverrideStep` 실제 코드.* *계산 공식 출처: `CommissionCalculator` / `TaxCalculator` / `CalcOverrideStep` + P7 7개 도메인 Service (V88~V96) 실제 코드.*
+26 -2
View File
@@ -377,7 +377,7 @@ PWA 스캐폴드에서 클라이언트가 `agentId` 를 쿼리 파라미터로
P5 전체 + P6 PoC + **P6 후속(2026-05-27)** 완료. 잔여는 외부 SDK 차단 항목만. P5 전체 + P6 PoC + **P6 후속(2026-05-27)** 완료. 잔여는 외부 SDK 차단 항목만.
### 운영 DB 마이그레이션 상태 ### 운영 DB 마이그레이션 상태
운영 DB `192.168.0.60:55432/trading_ai/ga` 현재 **V85 적용 완료** (변경 없음). 운영 DB `192.168.0.60:55432/trading_ai/ga` 현재 **V96 적용 완료** (P7 incar 갭 반영 7도메인).
ga-api 8082 / ga-frontend 3000 / ga-mobile-pwa 3001 모두 라이브. ga-api 8082 / ga-frontend 3000 / ga-mobile-pwa 3001 모두 라이브.
단위/통합 테스트: **SqlSessionFactory 환경 문제 해결 완료 (2026-05-29, §3-17)**. `./gradlew test` → 112건 전부 PASS (ga-common 57 / ga-api 36 / ga-batch 19). 단위/통합 테스트: **SqlSessionFactory 환경 문제 해결 완료 (2026-05-29, §3-17)**. `./gradlew test` → 112건 전부 PASS (ga-common 57 / ga-api 36 / ga-batch 19).
@@ -414,6 +414,30 @@ ga-api 8082 / ga-frontend 3000 / ga-mobile-pwa 3001 모두 라이브.
| 카카오 알림톡 본체 | 비즈채널·템플릿 승인 | | 카카오 알림톡 본체 | 비즈채널·템플릿 승인 |
| 푸시 알림 (FCM 등) | 외부 채널 결정 | | 푸시 알림 (FCM 등) | 외부 채널 결정 |
## 3-18. P7 — incar 갭 반영 수수료 계산 7개 도메인 풀스택 (V88~V96 — 완료, 2026-05-31 라이브 적용)
incar CMS 갭 분석 결과 GA 시스템에 없던 수수료 계산 7개 도메인을 DB→Core→API→Frontend 전 레이어로 추가. 운영 DB schema **v96**.
| 도메인 | 마이그레이션 | 테이블 | API 경로 |
|---|---|---|---|
| 수입수수료+수지차 (INCOME_COMM) | V88 | income_commission_ledger, margin_summary | `/api/income-commissions/*` |
| 소개·이관수수료 (REFERRAL_COMM) | V89 | referral_commission_rule, referral_commission_ledger | `/api/referral-commissions/*` |
| 정착지원금 (SETTLING_SUPPORT) | V90 | settling_support_rule, settling_support_ledger | `/api/settling-supports/*` |
| 인정실적+환산보험료 (RECOGNIZED_PERF) | V91 | product_conversion_rate, recognized_performance_ledger | `/api/recognized-performances/*` |
| 시상 (AWARD) | V92 | award_rule, award_ledger | `/api/awards/*` |
| 생보운영지원수수료 (LIFE_OP_SUPPORT) | V93 | life_support_rule, life_support_ledger | `/api/life-supports/*` |
| 지점장수당 (BRANCH_MGR_ALLOWANCE) | V94 | branch_mgr_allowance_rule, branch_mgr_allowance_ledger | `/api/branch-allowances/*` |
| 메뉴 7개 | V95 | — | GRP_COMMISSION 하위 sort 80~140 |
| 공통코드 5그룹 | V96 | — | INCOME_STATUS/REFERRAL_TYPE/CALC_TYPE/SUPPORT_STATUS/AWARD_TYPE |
통합 검증 (2026-05-31):
- `./gradlew build -x test` BUILD SUCCESSFUL
- Flyway V88~V96 9건 모두 success=True, 운영 DB schema v96
- 신규 14테이블 전부 존재 확인, 신규 메뉴 7건 등록 확인
- 기존 smoke_allmenus.py 67엔드포인트: PASS=66 FAIL=0 SKIP=1(settle/summary 필수파라미터, 기존 동일)
- P7 신규 8엔드포인트(인정실적은 /rates+/ledgers 분리): PASS=8 FAIL=0
- 주의: `/api/recognized-performances/rules` 는 없음. D4는 `/rates`(환산율)와 `/ledgers`(원장) 두 경로로 분리됨.
## 3-16. 계약 승계 (contract_succession, V86 — 완료, 2026-05-29 라이브 적용) ## 3-16. 계약 승계 (contract_succession, V86 — 완료, 2026-05-29 라이브 적용)
설계사 해촉/이관 시 보험계약의 **유지수수료(maintain) 귀속 설계사를 승계월 기준 후임으로 전환**. 설계사 해촉/이관 시 보험계약의 **유지수수료(maintain) 귀속 설계사를 승계월 기준 후임으로 전환**.
@@ -509,7 +533,7 @@ ga-api 8082 / ga-frontend 3000 / ga-mobile-pwa 3001 모두 라이브.
1. **Claude Code 시작 시 cwd 확인**: `C:\Users\kyu\Desktop\ga_pro` 1. **Claude Code 시작 시 cwd 확인**: `C:\Users\kyu\Desktop\ga_pro`
2. **HANDOFF.md 읽기**: 본 문서 §3-16(계약 승계) + §3-15(P6 후속) + 최근 git log 2. **HANDOFF.md 읽기**: 본 문서 §3-16(계약 승계) + §3-15(P6 후속) + 최근 git log
3. **운영 DB 상태**: **V86 적용 완료** (계약 승계). ga-api 8082 가동. ga-frontend 3000 / ga-mobile-pwa 3001 은 필요 시 기동. origin/main 동기화 완료(HEAD `4dbc688`, 2026-05-29 push). 3. **운영 DB 상태**: **V96 적용 완료** (P7 incar 7도메인). ga-api 8082 가동. ga-frontend 3000 / ga-mobile-pwa 3001 은 필요 시 기동. origin/main 동기화 완료(HEAD `4dbc688`, 2026-05-29 push). P7 코드(V88~V96)는 로컬 커밋 아직 없음.
4. **재기동이 필요한 경우**: 4. **재기동이 필요한 경우**:
- `cd ga-commission-system && nohup ./gradlew :ga-api:bootRun --args='--spring.profiles.active=trading_ai' > ../ga-api-bootrun.log 2>&1 & disown` - `cd ga-commission-system && nohup ./gradlew :ga-api:bootRun --args='--spring.profiles.active=trading_ai' > ../ga-api-bootrun.log 2>&1 & disown`
- `cd ga-commission-system/ga-frontend && npm run dev` (3000) - `cd ga-commission-system/ga-frontend && npm run dev` (3000)
+255
View File
@@ -0,0 +1,255 @@
# P7 — incar 갭 반영: 수수료 계산 7개 도메인
작성: PL (자율 모드), 2026-05-31
근거: incar CMS 116메뉴 vs ga-pro 96메뉴 갭 분석. 사용자 지시 "필요한 메뉴 적용 + 수수료 공식 다 적용".
범위: 🅰 수수료 계산 7개 도메인 풀스택 (DB→Core→API→Frontend→Infra).
기존 패턴 100% 준수 (template = 수금수수료 V41 / CollectionCommission*). 모든 금액 `NUMERIC(15,2)`, 율 `NUMERIC(7,4)`(소수, 0~1), 반올림 HALF_UP.
Flyway: **V87이 최신 → V88부터.** 각 도메인은 rule(+) ledger 2테이블 패턴. 메뉴는 GRP_COMMISSION 하위.
---
## 도메인별 설계
### D1. 수입수수료 + 수지차 (INCOME_COMM) — V88
보험사가 GA에 지급한 **수입** 측. 기존 원장(LEDGER_*)은 설계사 **지급** 측. 수지차 = 수입 지급.
**income_commission_ledger** (수입수수료 원장)
| 컬럼 | 타입 | 설명 |
|---|---|---|
| income_id | BIGSERIAL PK | |
| contract_id | BIGINT NN FK contract | |
| carrier_id | BIGINT NN FK insurance_company | 보험사 |
| settle_month | CHAR(6) NN | |
| premium_amount | NUMERIC(15,2) NN | 보험료 |
| company_rate | NUMERIC(7,4) NN | 보험사요율 |
| income_amount | NUMERIC(15,2) NN | **= premium_amount × company_rate** |
| commission_type | VARCHAR(20) NN | RECRUIT/MAINTAIN 등 |
| status | VARCHAR(20) NN DEFAULT 'CALCULATED' | CALCULATED/CONFIRMED/CANCELLED |
| created_at/updated_at | | |
idx: (carrier_id, settle_month), (contract_id), (status, settle_month)
**margin_summary** (수지차 집계 — recalc 액션으로 채움)
| 컬럼 | 타입 | 설명 |
|---|---|---|
| margin_id | BIGSERIAL PK | |
| settle_month | CHAR(6) NN | |
| agent_id | BIGINT FK agent (NULL이면 전체) | |
| income_total | NUMERIC(15,2) NN DEFAULT 0 | 수입 합 |
| payout_total | NUMERIC(15,2) NN DEFAULT 0 | 지급 합(settle_master 기준) |
| margin_amount | NUMERIC(15,2) NN DEFAULT 0 | **= income_total payout_total** |
| margin_rate | NUMERIC(7,4) | = margin/income (income>0) |
| created_at | | |
UNIQUE(settle_month, agent_id)
**공식**: income_amount = premium × company_rate. margin = Σincome Σpayout.
### D2. 소개·이관수수료 (REFERRAL_COMM) — V89
계약을 소개(REFERRAL)하거나 이관(TRANSFER)한 설계사에게 주는 수수료.
**referral_commission_rule**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| rule_id | BIGSERIAL PK | |
| referral_type | VARCHAR(20) NN | REFERRAL/TRANSFER |
| calc_type | VARCHAR(10) NN | RATE/FIXED |
| commission_rate | NUMERIC(7,4) | calc_type=RATE 일 때 |
| fixed_amount | NUMERIC(15,2) | calc_type=FIXED 일 때 |
| effective_from DATE NN / effective_to DATE | | |
CHECK referral_type IN ('REFERRAL','TRANSFER'); CHECK calc_type IN ('RATE','FIXED')
**referral_commission_ledger**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| ledger_id | BIGSERIAL PK | |
| contract_id | BIGINT NN FK | |
| referrer_agent_id | BIGINT NN FK agent | 소개/이관한 설계사 |
| settle_month | CHAR(6) NN | |
| referral_type | VARCHAR(20) NN | |
| base_amount | NUMERIC(15,2) NN | 기준금액(수수료/보험료) |
| commission_rate | NUMERIC(7,4) | |
| commission_amount | NUMERIC(15,2) NN | **= base×rate (RATE) 또는 fixed_amount (FIXED)** |
| status | VARCHAR(20) NN DEFAULT 'CALCULATED' | |
idx: (referrer_agent_id, settle_month),(contract_id)
### D3. 정착지원금 (SETTLING_SUPPORT) — V90
신입/경력 설계사 정착 지원금. 등급별 정액 지급 + 조기 해촉 시 환수.
**settling_support_rule**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| rule_id BIGSERIAL PK | | |
| agent_grade | VARCHAR(20) NN | 설계사 등급 |
| support_amount | NUMERIC(15,2) NN | 월 정액 지원금 |
| duration_months | INT NN | 약정 지원 개월수 |
| clawback_months | INT NN | 이 개월 내 해촉 시 환수 대상 |
| effective_from DATE NN / effective_to DATE | | |
**settling_support_ledger**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| ledger_id BIGSERIAL PK | | |
| agent_id BIGINT NN FK agent | | |
| settle_month CHAR(6) NN | | |
| support_amount NUMERIC(15,2) NN | 지급액 | |
| clawback_amount NUMERIC(15,2) NN DEFAULT 0 | 환수액 | |
| net_amount NUMERIC(15,2) NN | **= support_amount clawback_amount** | |
| status VARCHAR(20) NN DEFAULT 'CALCULATED' | PAID/CLAWED_BACK 포함 | |
idx: (agent_id, settle_month)
**공식**: 지급 = 등급별 support_amount. 환수 = 조기해촉시 support_amount × (잔여약정개월/약정개월) (clawback service 계산).
### D4. 인정실적 + 환산보험료 (RECOGNIZED_PERF) — V91
환산보험료 = 보험료 × 상품별환산율. 인정실적 = 환산보험료 × 인정율. 시상/등급의 기초.
**product_conversion_rate** (상품별 환산율)
| 컬럼 | 타입 | 설명 |
|---|---|---|
| rate_id BIGSERIAL PK | | |
| product_id BIGINT NN FK product | | |
| conversion_rate NUMERIC(7,4) NN | 환산율 (예: 0.5=50%) | |
| effective_from DATE NN / effective_to DATE | | |
UNIQUE(product_id, effective_from)
**recognized_performance_ledger**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| ledger_id BIGSERIAL PK | | |
| agent_id BIGINT NN FK agent | | |
| contract_id BIGINT NN FK contract | | |
| settle_month CHAR(6) NN | | |
| premium_amount NUMERIC(15,2) NN | 보험료 | |
| conversion_rate NUMERIC(7,4) NN | 적용 환산율 | |
| converted_premium NUMERIC(15,2) NN | **= premium × conversion_rate** | |
| recognition_rate NUMERIC(7,4) NN | 인정율 | |
| recognized_amount NUMERIC(15,2) NN | **= converted_premium × recognition_rate** | |
| status VARCHAR(20) NN DEFAULT 'CALCULATED' | | |
idx: (agent_id, settle_month),(contract_id)
### D5. 시상 (AWARD) — V92
신인도입/법인/유지율 등 시상. 조건 충족 시 정액 또는 정률.
**award_rule**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| rule_id BIGSERIAL PK | | |
| award_type VARCHAR(20) NN | NEW_RECRUIT/CORPORATE/RETENTION/PERFORMANCE | |
| calc_type VARCHAR(10) NN | RATE/FIXED | |
| threshold_amount NUMERIC(15,2) | 충족 기준 실적(이상) | |
| award_rate NUMERIC(7,4) | RATE | |
| award_fixed NUMERIC(15,2) | FIXED | |
| effective_from DATE NN / effective_to DATE | | |
CHECK award_type IN (...); CHECK calc_type IN ('RATE','FIXED')
**award_ledger**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| ledger_id BIGSERIAL PK | | |
| agent_id BIGINT NN FK agent | | |
| settle_month CHAR(6) NN | | |
| award_type VARCHAR(20) NN | | |
| base_amount NUMERIC(15,2) NN | 기준 실적 | |
| award_amount NUMERIC(15,2) NN | **= base×award_rate (RATE) 또는 award_fixed (FIXED)** | |
| status VARCHAR(20) NN DEFAULT 'CALCULATED' | | |
idx: (agent_id, settle_month),(award_type, settle_month)
### D6. 생보운영지원수수료 (LIFE_OP_SUPPORT) — V93
생보 계약 운영지원 수수료. 실적 기준 지급률 + 환수.
**life_support_rule**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| rule_id BIGSERIAL PK | | |
| agent_grade VARCHAR(20) NN | | |
| support_rate NUMERIC(7,4) NN | 지급률 | |
| effective_from DATE NN / effective_to DATE | | |
**life_support_ledger**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| ledger_id BIGSERIAL PK | | |
| agent_id BIGINT NN FK agent | | |
| settle_month CHAR(6) NN | | |
| base_amount NUMERIC(15,2) NN | 생보 실적 | |
| support_rate NUMERIC(7,4) NN | | |
| support_amount NUMERIC(15,2) NN | **= base × support_rate** | |
| clawback_amount NUMERIC(15,2) NN DEFAULT 0 | 환수 | |
| net_amount NUMERIC(15,2) NN | = support clawback | |
| status VARCHAR(20) NN DEFAULT 'CALCULATED' | | |
idx: (agent_id, settle_month)
### D7. 지점장수당 (BRANCH_MGR_ALLOWANCE) — V94
지점장이 소속 조직 실적에 대해 받는 수당. cap 제한 + 환수.
**branch_mgr_allowance_rule**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| rule_id BIGSERIAL PK | | |
| position_code VARCHAR(20) NN | 직책(지점장 등) | |
| allowance_rate NUMERIC(7,4) NN | 수당율 | |
| cap_amount NUMERIC(15,2) | 상한(NULL=무제한) | |
| effective_from DATE NN / effective_to DATE | | |
**branch_mgr_allowance_ledger**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| ledger_id BIGSERIAL PK | | |
| manager_agent_id BIGINT NN FK agent | 지점장 | |
| org_id BIGINT NN FK organization | 대상 조직 | |
| settle_month CHAR(6) NN | | |
| base_amount NUMERIC(15,2) NN | 조직 실적 | |
| allowance_rate NUMERIC(7,4) NN | | |
| allowance_amount NUMERIC(15,2) NN | **= LEAST(base×rate, cap)** | |
| clawback_amount NUMERIC(15,2) NN DEFAULT 0 | | |
| status VARCHAR(20) NN DEFAULT 'CALCULATED' | | |
idx: (manager_agent_id, settle_month),(org_id, settle_month)
> **확정 FK (DB 실검증):** contract(contract_id) · agent(agent_id) · product(product_id) · organization(org_id) · **insurance_company(company_id)** · settle_master. 보험사 FK는 `carrier_id BIGINT REFERENCES insurance_company(company_id)`. 수지차 payout은 settle_master 집계로 산출(dba가 컬럼 확인).
---
## 메뉴 (V95) — GRP_COMMISSION 하위 7개 PAGE
| menu_code | menu_name | menu_path | component_path | sort |
|---|---|---|---|---|
| INCOME_COMM | 수입수수료/수지차 | /commission/income | commission/IncomeCommission | 80 |
| REFERRAL_COMM | 소개·이관수수료 | /commission/referral | commission/ReferralCommission | 90 |
| SETTLING_SUPPORT | 정착지원금 | /commission/settling-support | commission/SettlingSupport | 100 |
| RECOGNIZED_PERF | 인정실적 | /commission/recognized | commission/RecognizedPerformance | 110 |
| AWARD | 시상 | /commission/award | commission/Award | 120 |
| LIFE_OP_SUPPORT | 생보운영지원수수료 | /commission/life-support | commission/LifeSupport | 130 |
| BRANCH_MGR_ALLOWANCE | 지점장수당 | /commission/branch-allowance | commission/BranchAllowance | 140 |
권한: READ/CREATE/UPDATE/DELETE/EXPORT. 역할부여: SUPER_ADMIN/ADMIN 전체, MANAGER READ/CREATE/UPDATE/EXPORT, AGENT READ(INCOME_COMM 제외 — 본인 관련만). V54 패턴 그대로.
## 공통코드 (V96)
| group_code | 코드 |
|---|---|
| INCOME_STATUS | CALCULATED/CONFIRMED/CANCELLED |
| REFERRAL_TYPE | REFERRAL=소개/TRANSFER=이관 |
| CALC_TYPE | RATE=정률/FIXED=정액 |
| SUPPORT_STATUS | CALCULATED/PAID/CLAWED_BACK |
| AWARD_TYPE | NEW_RECRUIT=신인도입/CORPORATE=법인/RETENTION=유지율/PERFORMANCE=실적 |
CommissionType enum 확장: INCOME/REFERRAL/TRANSFER/SETTLING/RECOGNIZED/AWARD/LIFE_SUPPORT/BRANCH_ALLOWANCE 추가 + V40 COMMISSION_TYPE 공통코드 동기화.
---
## Wave 실행
| Wave | 담당 | 산출물 |
|---|---|---|
| 1 | dba-architect | V88~V96 (도메인 7 + 메뉴 + 공통코드). FK 테이블명 실검증. |
| 2 | core-architect | 14테이블 VO/Resp/SaveReq/SearchParam + Mapper IF+XML + CommissionType enum 확장 |
| 3 | api-developer | 7 Service + 7 Controller (@RequirePermission/@DataChangeLog, 공식 계산 Service 내장) |
| 4 | frontend-developer | 7 페이지 + api/commission.ts 확장 + App.tsx 라우트 |
| 5 | infra-reviewer | `./gradlew build` + 프론트 빌드 + smoke. README/HANDOFF/DOMAIN_KNOWLEDGE 갱신 |
---
## 완료 상태 (2026-05-31)
schema **v96** 적용(Flyway V88~V96 success), 전체 `./gradlew build` SUCCESSFUL, 프론트 빌드 OK, ga-api 8082 라이브. P7 14개 GET + 수지차 recalc POST 전수 호출 5xx 0건. 수입수수료(보험료×요율=6,000,000)·수지차(수입−지급) 공식 런타임 실측 검증 후 테스트데이터 정리.
**PL 통합검증에서 잡은 버그 3건(팀원 GET-only smoke가 놓침) — 수정 완료:**
1. 프론트 인정실적 경로 `/rules` → 백엔드 `/rates` 불일치 → `api/commission.ts` 수정.
2. 프론트 수지차 경로 `/margin` → 백엔드 `/margins` 불일치 → 수정.
3. 수지차 recalc 3중 결함: 컨트롤러 `@RequestBody`(프론트는 쿼리파라미터)→`@RequestParam`, 서비스 `payoutTotal=0` 하드코딩 → settle_master.net_amount 실집계(`sumPayoutByMonth`)+`sumIncomeByMonth` 신규, `selectByMonthAndAgent``#{agentId} IS NULL` Postgres typed-null 오류 → `<choose>`로 교체.
+169
View File
@@ -0,0 +1,169 @@
# P8 — incar 갭 반영: 운영/정산 프로세스 7개 도메인 (🅱)
작성: PL (자율 모드), 2026-05-31. 사용자 지시 "다 적용". P7(🅰 수수료계산 7)에 이어 🅱 운영 7개로 incar 갭 14개 완성.
범위: 풀스택(DB→Core→API→Frontend→Infra). 패턴 100% 준수(template = 수금수수료 V41 / CollectionCommission*, P7과 동일).
Flyway: **V96이 최신 → V97부터.** 메뉴는 적절한 상위그룹 하위. 금액 NUMERIC(15,2), 율 NUMERIC(7,4), HALF_UP.
**확정 FK**: agent(agent_id), contract(contract_id), organization(org_id), insurance_company(company_id), grade(grade_id), settle_master.
---
## 도메인별 설계
### D8. 단계마감 (STEP_CLOSE) — V97
월 정산을 단계별로 마감(수신→계산→검증→지급). 기존 PERIOD_CLOSE(단일 잠금) 보강.
**settlement_step_close**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| step_close_id | BIGSERIAL PK | |
| settle_month | CHAR(6) NN | |
| step_code | VARCHAR(20) NN | RECEIVE/CALC/VERIFY/PAY |
| step_seq | INT NN | 단계 순서 1~4 |
| status | VARCHAR(20) NN DEFAULT 'OPEN' | OPEN/CLOSED |
| closed_at TIMESTAMP / closed_by BIGINT / created_at | | |
UNIQUE(settle_month, step_code); CHECK step_code IN(...); CHECK status IN('OPEN','CLOSED')
공식 없음(워크플로우). 액션: close(단계마감), reopen.
### D9. 본사정산 (HQ_SETTLE) — V98
GA 본사 자체의 수입/비용 정산 항목(설계사 지급과 별개의 GA 손익).
**hq_settle_item**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| item_id | BIGSERIAL PK | |
| settle_month | CHAR(6) NN | |
| item_type | VARCHAR(20) NN | INCOME(수입)/EXPENSE(비용) |
| item_name | VARCHAR(100) NN | 항목명 |
| amount | NUMERIC(15,2) NN | 금액 |
| memo | VARCHAR(300) | |
| status | VARCHAR(20) NN DEFAULT 'REGISTERED' | REGISTERED/CONFIRMED |
| created_at/created_by/updated_at | | |
idx: (settle_month, item_type). CHECK item_type IN('INCOME','EXPENSE')
공식: 본사손익 = Σ(INCOME amount) Σ(EXPENSE amount). 조회 집계 엔드포인트 제공.
### D10. 등급평가 (GRADE_EVAL) — V99
설계사 실적 점수로 등급 산정. rule(점수구간→등급) + 평가원장.
**grade_eval_rule**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| rule_id BIGSERIAL PK | | |
| grade_id | BIGINT NN FK grade | 부여 등급 |
| min_score | NUMERIC(7,2) NN | 이 점수 이상이면 해당 등급 |
| effective_from DATE NN / effective_to DATE | | |
**agent_grade_evaluation**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| eval_id BIGSERIAL PK | | |
| agent_id BIGINT NN FK agent | | |
| eval_period CHAR(6) NN | 평가기준월 | |
| performance_score NUMERIC(7,2) NN | 실적점수 | |
| prev_grade_id BIGINT FK grade | 기존 등급 | |
| evaluated_grade_id BIGINT FK grade | 산정 등급 | |
| status VARCHAR(20) NN DEFAULT 'EVALUATED' | EVALUATED/APPLIED | |
idx: (agent_id, eval_period). UNIQUE(agent_id, eval_period)
공식: evaluated_grade = score 이상인 최대 min_score 룰의 grade. Service가 grade_eval_rule 조회해 결정.
### D11. 공동계약 (JOINT_CONTRACT) — V100
한 계약을 여러 설계사가 공동 모집 → 지분율(share_rate)로 수수료 배분.
**joint_contract**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| joint_id BIGSERIAL PK | | |
| contract_id BIGINT NN FK contract | | |
| agent_id BIGINT NN FK agent | | |
| role VARCHAR(10) NN | LEAD/SUB | |
| share_rate NUMERIC(7,4) NN | 지분율(합=1) | |
| created_at/created_by | | |
UNIQUE(contract_id, agent_id); CHECK role IN('LEAD','SUB'); CHECK share_rate>=0 AND share_rate<=1
공식: 설계사몫 = 계약수수료 × share_rate. (검증: 계약별 share_rate 합=1 — Service 가드)
### D12. 보증보험 (GUARANTEE_INS) — V101
설계사가 가입하는 신원보증보험 가입현황 추적(공식 없음, 관리).
**agent_guarantee_insurance**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| guarantee_id BIGSERIAL PK | | |
| agent_id BIGINT NN FK agent | | |
| policy_no VARCHAR(50) NN | 증권번호 | |
| carrier_id BIGINT FK insurance_company(company_id) | 보증보험사 | |
| guarantee_amount NUMERIC(15,2) NN | 보증금액 | |
| start_date DATE NN / end_date DATE NN | 보증기간 | |
| status VARCHAR(20) NN DEFAULT 'ACTIVE' | ACTIVE/EXPIRED/CANCELLED | |
idx: (agent_id, status),(end_date)
### D13. 적립금 (RESERVE_FUND) — V102
환수 대비 수수료 일부 적립 + 잔액 관리.
**agent_reserve_fund**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| reserve_id BIGSERIAL PK | | |
| agent_id BIGINT NN FK agent | | |
| settle_month CHAR(6) NN | | |
| reserve_in NUMERIC(15,2) NN DEFAULT 0 | 적립액 | |
| reserve_out NUMERIC(15,2) NN DEFAULT 0 | 인출/사용액 | |
| balance NUMERIC(15,2) NN | **= 전월잔액 + reserve_in reserve_out** | |
| created_at | | |
UNIQUE(agent_id, settle_month). idx (agent_id, settle_month)
공식: balance = (직전월 balance) + reserve_in reserve_out. Service가 직전월 조회해 누적.
### D14. 수수료 시뮬레이터 (COMM_SIMULATOR) — V103
보험료/요율/지급률 입력 → 수수료·세금·실지급·분급·환수예상 산출. 시나리오 저장.
**commission_simulation**
| 컬럼 | 타입 | 설명 |
|---|---|---|
| sim_id BIGSERIAL PK | | |
| sim_name VARCHAR(100) NN | 시나리오명 | |
| premium_amount NUMERIC(15,2) NN | 입력 보험료 | |
| company_rate NUMERIC(7,4) NN | 보험사요율 | |
| payout_rate NUMERIC(7,4) NN | 지급률 | |
| commission_year INT NN | 회차 | |
| company_payment NUMERIC(15,2) NN | = 보험료×요율 | |
| agent_commission NUMERIC(15,2) NN | = 보험사지급액×지급률 | |
| withholding_tax NUMERIC(15,2) NN | = 설계사수수료×3.3% | |
| net_amount NUMERIC(15,2) NN | = 수수료−원천세 | |
| expected_chargeback NUMERIC(15,2) | 예상 환수(13개월 가정 50% 등) | |
| created_at/created_by | | |
공식(DOMAIN_KNOWLEDGE 7장과 동일): company_payment=premium×company_rate, agent_commission=company_payment×payout_rate, withholding_tax=agent_commission×0.033, net=agent_commissionwithholding_tax. Service의 simulate 액션이 입력만 받아 계산 후 저장/반환.
---
## 메뉴 (V104)
| menu_code | menu_name | 상위그룹 | menu_path | component_path |
|---|---|---|---|---|
| STEP_CLOSE | 단계마감 | GRP_SETTLE | /settle/step-close | settle/StepClose |
| HQ_SETTLE | 본사정산 | GRP_SETTLE | /settle/hq-settle | settle/HqSettle |
| GRADE_EVAL | 등급평가 | GRP_ORG | /org/grade-eval | org/GradeEvaluation |
| JOINT_CONTRACT | 공동계약 | GRP_PRD | /contracts/joint | contracts/JointContract |
| GUARANTEE_INS | 보증보험 | GRP_ORG | /org/guarantee | org/GuaranteeInsurance |
| RESERVE_FUND | 적립금 | GRP_COMMISSION | /commission/reserve | commission/ReserveFund |
| COMM_SIMULATOR | 수수료 시뮬레이터 | GRP_COMMISSION | /commission/simulator | commission/CommissionSimulator |
권한 READ/CREATE/UPDATE/DELETE/EXPORT(STEP_CLOSE는 READ/UPDATE/APPROVE). 역할부여 V54/V95 패턴.
## 공통코드 (V105)
| group_code | 코드 |
|---|---|
| STEP_CODE | RECEIVE=수신마감/CALC=계산마감/VERIFY=검증마감/PAY=지급마감 |
| STEP_STATUS | OPEN=열림/CLOSED=마감 |
| HQ_ITEM_TYPE | INCOME=수입/EXPENSE=비용 |
| JOINT_ROLE | LEAD=주설계사/SUB=공동설계사 |
| GUARANTEE_STATUS | ACTIVE=유효/EXPIRED=만료/CANCELLED=해지 |
| GRADE_EVAL_STATUS | EVALUATED=평가완료/APPLIED=반영완료 |
---
## Wave 실행 (P7과 동일 팀/패턴)
1 dba: V97~V105 · 2 core: VO/Mapper/Enum · 3 api: Service/Controller(공식·집계 내장) · 4 front: 7페이지+commission/settle/org api+App.tsx · 5 infra: build+smoke
**PL 검증**: GET뿐 아니라 액션 POST(close/reopen/simulate/recalc balance/등급산정)까지 실호출로 5xx 0 확인 — P7에서 GET-only smoke가 버그를 놓친 교훈 반영.
---
## 완료 상태 (2026-05-31)
schema **v105** 적용(Flyway V97~V105 success), 전체 `./gradlew build` SUCCESSFUL, 프론트 빌드 OK, ga-api 8082 라이브. 8테이블·7메뉴 등록. GET 9 + 액션 POST 5(simulate/hq-settles/step-closes/close/reserve upsert) **전부 200, 5xx 0건**. 시뮬레이터 공식 실측: 보험료 1,000,000×0.6=600,000 → ×0.75=450,000 → 원천세 14,850 → net 435,150 → 예상환수 225,000 ✓. 테스트데이터 정리 완료. (P8은 P7과 달리 액션까지 PL이 직접 호출 검증 → 숨은 버그 0.)
@@ -0,0 +1,349 @@
# 신입사원 완전 기초 교육 — 보험과 수수료, 0부터 시작하기
> 대상: **보험을 한 번도 다뤄본 적 없는 신입사원/개발자.**
> 목표: 이 문서 하나만 끝까지 읽으면 "우리 회사가 무슨 일을 하고, 수수료가 어떻게 계산돼서 설계사 통장에 꽂히는지" 머릿속에 그림이 그려지게 하는 것.
> 읽는 법: 위에서 아래로 순서대로. 어려운 용어는 나올 때마다 바로 쉬운 말로 풀어 씁니다. 숫자 예시는 직접 손으로 따라 계산해 보세요.
> 더 깊은 코드/규정 레벨은 [`DOMAIN_KNOWLEDGE.md`](DOMAIN_KNOWLEDGE.md) 참고.
---
## 목차
1. [보험이 뭔가요? (진짜 0단계)](#1-보험이-뭔가요-진짜-0단계)
2. [등장인물 4명 — 누가 누구에게 돈을 주나](#2-등장인물-4명--누가-누구에게-돈을-주나)
3. [보험 기초 용어 25개 — 이것만 알면 대화가 된다](#3-보험-기초-용어-25개--이것만-알면-대화가-된다)
4. [수수료가 뭐고 왜 종류가 많은가](#4-수수료가-뭐고-왜-종류가-많은가)
5. [수수료 종류 한눈에 (우리 시스템이 다루는 전부)](#5-수수료-종류-한눈에-우리-시스템이-다루는-전부)
6. [수수료 계산의 핵심 공식 — "세 번 곱하기"](#6-수수료-계산의-핵심-공식--세-번-곱하기)
7. [숫자로 끝까지 따라가기 (한 계약의 일생)](#7-숫자로-끝까지-따라가기-한-계약의-일생)
8. [그냥 곱하면 안 되는 이유 — 규제 3종](#8-그냥-곱하면-안-되는-이유--규제-3종)
9. [세금과 공제 — 통장에 적게 들어오는 이유](#9-세금과-공제--통장에-적게-들어오는-이유)
10. [한 달이 흘러가는 과정 — 월 정산 파이프라인](#10-한-달이-흘러가는-과정--월-정산-파이프라인)
11. [수입 vs 지급, 그리고 수지차 — 회사는 어디서 남기나](#11-수입-vs-지급-그리고-수지차--회사는-어디서-남기나)
12. [자주 나오는 질문 (FAQ)](#12-자주-나오는-질문-faq)
13. [용어 빠른 사전](#13-용어-빠른-사전)
---
## 1. 보험이 뭔가요? (진짜 0단계)
**보험 = 여럿이 조금씩 돈을 모아뒀다가, 사고를 당한 사람에게 몰아주는 약속.**
예를 들어 100명이 매달 1만 원씩 모읍니다(월 100만 원). 그 중 한 명이 큰 병에 걸리면, 모인 돈에서 큰 금액을 그 사람에게 줍니다. 나머지 99명은 "내가 안 걸려서 다행이다" 하고 넘어갑니다. 이 "돈 모으고 → 사고나면 주기"를 전문으로 운영하는 회사가 **보험사**입니다.
- 고객이 매달 내는 돈 = **보험료(premium)**
- 사고/사망/만기 때 받는 돈 = **보험금**
- 이 약속을 적은 계약서 한 장 = **보험계약** (증권 1장 = 계약 1건)
> 우리 회사는 보험사가 아닙니다. 우리는 **보험을 파는 일(영업)을 대행**하고, 그 대가로 받은 돈을 **영업한 사람들에게 정확히 나눠주는 회사**입니다. 그게 다음 장에서 나오는 GA입니다.
---
## 2. 등장인물 4명 — 누가 누구에게 돈을 주나
| 누구 | 한 줄 설명 | 비유 |
|---|---|---|
| **고객** | 보험에 가입하고 보험료를 내는 사람 | 물건을 사는 손님 |
| **보험사 (Carrier)** | 보험 상품을 만들고, 팔아준 대가로 수수료를 주는 회사 (삼성생명, 메리츠 등) | 물건을 만든 제조사 |
| **GA (우리 회사)** | 여러 보험사 상품을 한데 모아 파는 대형 보험대리점 | 여러 브랜드를 파는 백화점 |
| **설계사 (Agent)** | 실제로 고객을 만나 계약을 따오는 사람 | 백화점 매장 직원 |
**돈의 흐름 (제일 중요한 그림):**
```
[고객] ──보험료──▶ [보험사] ──수수료──▶ [GA(우리)] ──정산──▶ [설계사]
└ 세금 떼고, 공제 빼고 통장으로
```
- 보험사는 직접 영업하는 대신, GA·설계사가 고객을 데려와 주면 그 대가로 **수수료**를 줍니다.
- GA는 보험사한테 받은 수수료(=**수입수수료**)에서, 영업한 설계사 몫(=**지급수수료**)을 떼어 줍니다.
- **우리 시스템이 하는 일 = 이 "설계사 몫을 정확히 계산해서 나눠주는 것(정산)".**
---
## 3. 보험 기초 용어 25개 — 이것만 알면 대화가 된다
| 용어 | 쉬운 설명 |
|---|---|
| 보험계약 | 고객이 "매달 낼 테니 사고 때 보장해줘"라고 맺은 약속. 증권 1장 = 1건. |
| 보험료(premium) | 고객이 내는 돈. **모든 수수료 계산의 출발점.** 월납/연납/일시납. |
| 보험금 | 사고·만기 때 고객이 받는 돈. (수수료와는 다른 개념!) |
| 계약자 / 피보험자 / 수익자 | 돈 내는 사람 / 보장 대상 / 보험금 받는 사람. (셋이 같을 수도 다를 수도) |
| 생명보험 | 사람의 사망·생존·연금 관련 보험. (종신, 연금보험 등) |
| 손해보험 | 자동차·화재·실손 등 재산·사고 관련 보험. |
| 회차 / 차년(commission year) | 계약이 몇 년째인지. **1차년(초년도)** 수수료가 가장 큼. |
| 납입 / 미납 / 실효 | 보험료를 냈다 / 안 냈다 / 안 내서 계약이 끊겼다. |
| 유지 / 해지 / 부활 | 계약 살아있음 / 고객이 끊음 / 끊긴 걸 되살림. |
| 유지율(persistency) | 가입한 계약이 13개월·25개월 뒤에도 살아있는 비율. **영업 건전성의 핵심 지표.** |
| 청약 / 인수(언더라이팅) | 고객이 가입 신청 → 보험사가 위험 심사 후 받아들임. |
| 위촉 / 해촉 | 설계사가 GA에 소속됨 / 소속이 끝남. |
| 승계 | 설계사가 해촉되면 그가 관리하던 계약을 후임에게 넘기는 것. |
| 모집 | 설계사가 새 계약을 따오는 행위. |
| 환산보험료 | 상품마다 조건이 달라 비교가 어렵기 때문에, 정해진 "환산율"을 곱해 **공통 기준으로 환산한 보험료**. 실적 비교·시상의 기준. |
| 인정실적 | 회사가 "이건 실적으로 인정한다"고 집계한 환산보험료 합. 등급·시상의 근거. |
| 등급 | 설계사 레벨. 등급이 높을수록 지급률(받는 비율)이 높음. |
| 오버라이드 | 부하 설계사 실적에 대해 상위 직책자가 받는 수당. |
| 시상 | 목표 달성/우수자에게 주는 상금 성격의 수당. |
| 시책(인센티브) | 한시적 캠페인 보너스. |
| 환수(chargeback) | 계약이 일찍 깨지면 이미 준 수수료를 일부 돌려받는 것. |
| 분급 | 큰 수수료를 한 번에 안 주고 여러 해에 나눠 주는 것. |
| 정산 | 매월 수수료를 계산·확정·지급하는 업무 전체. |
| 원장(ledger) | 계산의 최소 단위 기록 (계약 × 월 × 수수료종류 1줄). |
| 명세서 | 설계사에게 "이번 달 얼마 받는지" 보여주는 내역서. |
---
## 4. 수수료가 뭐고 왜 종류가 많은가
**수수료 = 계약을 따오고(모집) 잘 유지시킨(유지) 대가로 보험사→GA→설계사로 흐르는 돈.**
왜 종류가 많을까요? 계약은 **태어나서(체결) → 유지되다가 → 갱신·부활도 하고 → 깨지기도(해지) 합니다.** 이 생애주기 단계마다, 또 영업을 도운 방식마다 다른 이름의 수수료가 붙기 때문입니다.
핵심 직관 하나만 기억하세요:
> **"초년도(1차년) 모집수수료가 제일 크고, 그 다음부터는 유지·관리 대가로 잘게 나뉜다."**
---
## 5. 수수료 종류 한눈에 (우리 시스템이 다루는 전부)
우리 시스템은 아래 수수료/수당을 모두 계산합니다. (★ = incar 벤치마킹으로 새로 추가한 것 — P7/P8)
### 5-1. 계약 생애주기 기반
| 종류 | 언제 | 누구에게 | 공식 한 줄 |
|---|---|---|---|
| 모집수수료 | 계약 체결(초년도) | 원 모집설계사 | 보험사지급액 × 지급률 |
| 유지수수료 | 유지되는 매월/매년 | 현재 관리설계사(승계 시 후임) | 보험사지급액 × 지급률 |
| 수금수수료 | 보험료가 실제 납입될 때 | 관리설계사 | 납입보험료 × 수금율 |
| 갱신수수료 | 갱신형 상품 갱신 시 | 관리설계사 | 갱신보험료 × 갱신율 |
| 부활수수료 | 실효 계약 부활 시 | 관리설계사 | 부활보험료 × 부활율 |
### 5-2. 성과·조직 기반
| 종류 | 의미 | 공식 한 줄 |
|---|---|---|
| 오버라이드 | 하위 실적에 대한 상위 직책 수당 | 하위실적 × 오버라이드율 (cap 제한) |
| 유지보수수당(지속수당) | 일정 유지율 달성 보너스 | 등급별 정액 또는 정률 |
| 시책(인센티브) | 캠페인/목표 달성 프로모션 | 캠페인 규칙대로 |
| ★ 시상(Award) | 신인도입/법인/유지율 등 시상 | 충족 시 실적×율 또는 정액 |
| ★ 지점장수당 | 지점장이 소속 조직 실적에 받는 수당 | min(조직실적 × 수당율, 한도) |
### 5-3. 소개·이관·지원 (★ 신규)
| 종류 | 의미 | 공식 한 줄 |
|---|---|---|
| ★ 소개수수료 | 계약을 소개해 준 설계사에게 | 기준금액 × 소개율 (또는 정액) |
| ★ 이관수수료 | 계약을 다른 설계사에게 이관할 때 | 기준금액 × 이관율 |
| ★ 정착지원금 | 신입/경력 설계사 정착 지원금(조기해촉 시 환수) | 등급별 정액 − 환수 |
| ★ 생보운영지원수수료 | 생보 운영지원 명목 수수료 | 실적 × 지급률 − 환수 |
### 5-4. 회사(GA) 레벨 (★ 신규)
| 종류 | 의미 | 공식 한 줄 |
|---|---|---|
| ★ 수입수수료 | 보험사가 GA에 준 돈(수입 측) | 보험료 × 보험사요율 |
| ★ 수지차 | GA가 남긴 마진 | 수입 합 − 지급 합 |
| ★ 인정실적 | 시상·등급의 기준이 되는 실적 | 환산보험료 × 인정율 |
| ★ 본사정산 | GA 본사 자체 수입/비용 정산 | Σ수입 − Σ비용 |
### 5-5. 보조 관리 (★ 신규, 공식보다 관리 중심)
| 항목 | 의미 |
|---|---|
| ★ 환산율(상품별) | 보험료를 공통 기준으로 환산하는 비율 |
| ★ 등급평가 | 실적 점수로 설계사 등급 산정 |
| ★ 공동계약 | 한 계약을 여러 설계사가 공동 모집, 지분율로 배분 |
| ★ 보증보험 | 설계사 신원보증보험 가입 관리 |
| ★ 적립금 | 환수 대비 수수료 일부 적립·잔액 관리 |
| ★ 단계마감 | 정산을 수신→계산→검증→지급 단계로 마감 |
| ★ 수수료 시뮬레이터 | 보험료/요율 입력 → 수수료·세금·실지급 미리 계산 |
> **모집 vs 유지의 귀속 차이(자주 묻는 함정):** 설계사가 해촉·이관되면 그 계약의 **유지수수료는 후임**에게 넘어가지만, **모집수수료는 원래 모집한 설계사**에게 그대로 남습니다.
---
## 6. 수수료 계산의 핵심 공식 — "세 번 곱하기"
대부분의 수수료는 **곱하기 세 번**이면 끝납니다. 이것만 외우면 80%는 이해한 겁니다.
```
① 보험사 지급액 = 보험료(premium) × 보험사요율(company_rate)
② 설계사 수수료 = 보험사 지급액 × 지급률(payout_rate)
③ 원천세 = 설계사 수수료 × 3.3%
──────────────────────────────────────────────
설계사 손에 = 설계사 수수료 − 원천세 (− 공제 + 부가세)
```
두 가지 율의 뜻:
- **보험사요율(company_rate)**: 보험사가 이 상품에 주기로 한 비율. **상품 × 회차(차년)**로 정해짐.
- **지급률(payout_rate)**: GA가 받은 돈 중 설계사에게 주는 비율. **설계사 등급 × 보험종류 × 회차**로 정해짐. → 같은 계약이라도 **등급 높은 설계사가 더 많이** 받음.
> 모집수수료와 유지수수료는 **공식이 똑같고**, 회차(commission_year)만 다릅니다. (모집 = 1차년, 유지 = 2차년 이후)
---
## 7. 숫자로 끝까지 따라가기 (한 계약의 일생)
연환산 보험료 **100만 원**짜리 계약 하나가 태어나서 깨질 때까지 따라가 봅시다.
### (A) 모집수수료 — 계약이 태어났다
| 항목 | 값 | 계산 |
|---|---|---|
| 보험료(연환산) | 1,000,000원 | (가정) |
| 보험사요율 | 60% | 상품·1차년 |
| **① 보험사 지급액** | **600,000원** | 1,000,000 × 60% |
| 지급률(이 설계사 등급) | 75% | 등급·1차년 |
| **② 설계사 수수료** | **450,000원** | 600,000 × 75% |
| **③ 원천세(3.3%)** | **14,850원** | 450,000 × 3.3% |
| **원장에 남는 실수령** | **435,150원** | 450,000 14,850 |
### (B) 분급 — 한 번에 안 주고 7년에 나눠 준다
큰 모집수수료(450,000원)는 규제상 7년에 걸쳐 비율대로 나눠 줍니다(35/25/15/10/7/5/3%).
| 차년 | 비율 | 지급액 |
|---|---|---|
| 1차년 | 35% | 157,500원 |
| 2차년 | 25% | 112,500원 |
| 3차년 | 15% | 67,500원 |
| … | … | … |
| **합계** | **100%** | **450,000원** |
→ 계약 직후 통장에 들어오는 건 450,000원이 아니라 **1차년분 157,500원(에서 세금 차감)**.
### (C) 환수 — 고객이 13개월 만에 해지했다
유지 13~24개월 구간 환수율 **50%** 적용:
```
환수액 = 기지급 모집수수료 × 환수율 = 450,000 × 50% = 225,000원 ← 다음 정산에서 (−)로 차감
```
- 12개월 이내면 100%(전액 환수), 85개월 이상이면 0%(환수 없음).
### (D) 오버라이드 — 이 설계사의 팀장이 받는 수당
```
오버라이드 = 하위 실적 × 오버라이드율 = 2,000,000 × 4% = 80,000원 (cap 넘으면 cap까지만)
```
### (E) 수지차 — 회사는 이 계약으로 얼마 남겼나 (★신규)
```
수입수수료(보험사가 GA에 준 돈) = 1,000,000 × 60% = 600,000원
지급수수료(GA가 설계사에 준 돈) = 450,000원
수지차(GA 마진) = 600,000 450,000 = 150,000원
```
이 한 흐름이 머리에 들어왔다면, 나머지 수수료 종류는 "기준금액 × 율" 변형일 뿐입니다.
---
## 8. 그냥 곱하면 안 되는 이유 — 규제 3종
수수료는 법으로 한도·방식이 정해져 있어, 단순 곱셈 결과를 그대로 줄 수 없습니다.
1. **1200% 룰** (보험업감독규정): 초년도 모집수수료 총액은 **연납보험료의 1200%**를 못 넘음. 시스템이 누적 지급액을 추적해 초과분을 잘라냄.
2. **분급**: 큰 수수료를 7년에 나눠 지급(위 7-B).
3. **차등 환수**: 조기 해지 시 유지기간이 짧을수록 많이 환수(위 7-C).
> 회사마다 세율·한도가 달라도 **코드는 안 고칩니다.** 전부 데이터(`system_config`, `regulatory_limit`, `installment_ratio`, `chargeback_grade`)로 빼놨기 때문에 값만 바꾸면 됩니다.
---
## 9. 세금과 공제 — 통장에 적게 들어오는 이유
설계사에게 주는 돈(gross)에서 **세금을 먼저 떼고(원천징수)**, **공제를 빼면** 실제 입금액(net)이 됩니다.
| 항목 | 율 | 설명 |
|---|---|---|
| 사업소득 원천징수 | 3.3% | 일반 설계사(사업소득자) |
| 기타소득 원천징수 | 8.8% | 기타소득 유형 |
| 지방소득세 | 소득세의 10% | 소득세에 부가 |
| 부가가치세 | 10% | 과세사업자가 세금계산서 발행 시 GA가 추가 지급(+) |
**공제(deduction)**: 미리 준 돈(가지급금) 회수, 대출 상환, **적립금** 등을 그 달 정산액에서 차감.
```
gross − 원천세(소득세+지방세) − 공제 (+부가세, 과세사업자) = net(실지급액) → 계좌이체(펌뱅킹)
```
**예시(과세사업자, 수수료 450,000원):** 소득세 14,850 + 지방세 1,485 빼고, 부가세 45,000 더하면 → **실지급 478,665원.**
---
## 10. 한 달이 흘러가는 과정 — 월 정산 파이프라인
매월 배치가 아래 순서로 돈다고 생각하면 됩니다.
```
보험사 데이터 수신 → 표준포맷 변환 → 대사(검증) →
모집수수료 계산 → 유지수수료 계산(승계 반영) → 환수·예외 →
오버라이드 계산 → 집계(설계사×월, 세금·공제 반영) →
회계 분개 → (분기/연말이면) 세무신고 자료
```
마감은 **단계마감(★신규)**으로 수신→계산→검증→지급 단계별로 잠급니다. 한 달을 닫으면(마감) 더는 못 고치고, 오류는 **정정 전표**로만 바로잡습니다(원본 보존). 설계사가 금액에 이의를 제기하면 **이의신청**으로 받습니다.
**상태 흐름:** `DRAFT(계산됨) → CONFIRMED(확정) → PAID(지급완료)`. 확정·지급된 건은 재정산해도 안 바뀝니다(보호).
---
## 11. 수입 vs 지급, 그리고 수지차 — 회사는 어디서 남기나 (★ 신규 핵심)
신입이 가장 헷갈리는 지점입니다. 두 방향을 구분하세요.
| 방향 | 누가 → 누구 | 시스템 용어 |
|---|---|---|
| **수입(income)** | 보험사 → GA | 수입수수료 |
| **지급(payout)** | GA → 설계사 | 모집/유지/… 수수료, 정산 |
```
수지차(마진) = Σ수입수수료 − Σ지급수수료
```
- 수지차가 (+)면 GA가 남긴 것, (−)면 그달 지급이 수입보다 많았던 것(분급·시상 등으로 일시적으로 발생 가능).
- 여기에 **본사정산**(GA 자체 수입/비용)까지 합치면 회사 전체 손익이 보입니다.
- 우리 시스템의 `수입수수료/수지차` 화면에서 **재계산 버튼**을 누르면 그달 수입 합과 지급 합(settle_master 기준)을 모아 수지차를 계산합니다.
---
## 12. 자주 나오는 질문 (FAQ)
1. **"수수료를 왜 한 번에 안 주죠?"** → 1200% 룰·분급 규제 + 조기해지 환수 리스크 때문. 7년 분할이 표준.
2. **"설계사가 그만두면 그 계약 수수료는?"** → 유지수수료는 후임(승계)에게, 모집수수료는 원 설계사에게.
3. **"gross인데 통장엔 적게 들어와요."** → 원천세(3.3%) + 공제를 뺀 net이 실지급액. (과세사업자는 부가세가 더해짐)
4. **"같은 계약인데 설계사마다 수수료가 달라요."** → 지급률이 **설계사 등급**에 따라 다르기 때문.
5. **"수입수수료랑 지급수수료가 헷갈려요."** → 수입 = 보험사가 우리에게 준 돈, 지급 = 우리가 설계사에게 준 돈. 차이가 수지차(마진).
6. **"환산보험료는 왜 필요해요?"** → 상품마다 보험료 조건이 달라 그대로 비교하면 불공평. 환산율을 곱해 공통 기준으로 맞춘 게 환산보험료고, 그 합이 인정실적이 되어 시상·등급의 잣대가 됩니다.
7. **"표준 세율/한도가 우리 회사랑 달라요."** → 코드 수정 불필요. 데이터(`system_config` 등)만 변경.
---
## 13. 용어 빠른 사전
| 한글 | 영문 | 한 줄 |
|---|---|---|
| 보험료 | Premium | 고객이 내는 돈. 계산의 출발점 |
| 보험금 | Benefit | 사고·만기 때 고객이 받는 돈 |
| 회차/차년 | Commission year | 계약 몇 년째. 1차년이 가장 큼 |
| 유지율 | Persistency | 13·25개월 뒤 계약 생존율 |
| 위촉/해촉 | Appoint/Terminate | 설계사 소속 시작/종료 |
| 승계 | Succession | 해촉 시 후임 귀속 전환 |
| 보험사요율 | Company rate | 보험사가 GA에 주는 비율 |
| 지급률 | Payout rate | GA가 설계사에 주는 비율(등급별) |
| 모집수수료 | Recruit | 체결 초년도 수수료(제일 큼) |
| 유지수수료 | Maintain | 유지 기간 수수료 |
| 수금수수료 | Collection | 보험료 납입 시 |
| 갱신/부활수수료 | Renewal/Reinstatement | 갱신·부활 시 |
| 오버라이드 | Override | 상위 직책 수당 |
| 시상 | Award | 목표 달성 상금 |
| 소개/이관수수료 | Referral/Transfer | 소개·이관 대가 |
| 정착지원금 | Settling support | 신입 정착 지원금 |
| 수입수수료 | Income commission | 보험사가 GA에 준 돈 |
| 수지차 | Margin | 수입 지급 (GA 마진) |
| 환산보험료 | Converted premium | 공통 기준 환산 보험료 |
| 인정실적 | Recognized performance | 시상·등급 기준 실적 |
| 본사정산 | HQ settlement | GA 자체 수입/비용 정산 |
| 적립금 | Reserve fund | 환수 대비 적립·잔액 |
| 분급 | Installment | 수수료 7년 분할 |
| 차등환수 | Chargeback | 조기 해지 환수 |
| 원천징수 | Withholding | 세금 미리 떼기(3.3%) |
| 공제 | Deduction | 가지급금 등 차감 |
| 단계마감 | Step close | 단계별 정산 잠금 |
| 정산 | Settlement | 월별 계산·확정·지급 |
| 원장 | Ledger | 계산 최소 단위 기록 |
| 명세서 | Statement | 설계사 지급 내역서 |
| 펌뱅킹 | Firm banking | 대량 계좌이체 |
---
*기준: 한국 보험업감독규정 표준 가정값. 시스템 schema (incar 갭 반영 P7+P8 14개 도메인 포함). 표준값은 데이터로 외부화되어 코드 변경 없이 조정 가능.*
*더 깊은 코드/배치/규정 레벨은 [`DOMAIN_KNOWLEDGE.md`](DOMAIN_KNOWLEDGE.md), [`DOMAIN_GAP_P7.md`](DOMAIN_GAP_P7.md), [`DOMAIN_GAP_P8.md`](DOMAIN_GAP_P8.md) 참고.*
@@ -0,0 +1,82 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.AwardService;
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/awards")
@RequiredArgsConstructor
public class AwardController {
private final AwardService service;
// ── Rule ──────────────────────────────────────────────────────────────────
@GetMapping("/rules")
@RequirePermission(menu = "AWARD", perm = "READ")
public ApiResponse<PageResponse<AwardRuleResp>> listRules(
@ModelAttribute AwardRuleSearchParam param) {
return ApiResponse.ok(service.listRules(param));
}
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "AWARD", perm = "READ")
public ApiResponse<AwardRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
@PostMapping("/rules")
@RequirePermission(menu = "AWARD", perm = "CREATE")
@DataChangeLog(menu = "AWARD", table = "award_rule")
public ApiResponse<Long> createRule(@Valid @RequestBody AwardRuleSaveReq req) {
return ApiResponse.ok(service.createRule(req));
}
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "AWARD", perm = "UPDATE")
@DataChangeLog(menu = "AWARD", table = "award_rule")
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
@Valid @RequestBody AwardRuleSaveReq req) {
service.updateRule(ruleId, req);
return ApiResponse.ok();
}
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "AWARD", perm = "DELETE")
@DataChangeLog(menu = "AWARD", table = "award_rule")
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
service.deleteRule(ruleId);
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
@GetMapping("/ledgers")
@RequirePermission(menu = "AWARD", perm = "READ")
public ApiResponse<PageResponse<AwardLedgerResp>> listLedgers(
@ModelAttribute AwardLedgerSearchParam param) {
return ApiResponse.ok(service.listLedgers(param));
}
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "AWARD", perm = "READ")
public ApiResponse<AwardLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
@PostMapping("/ledgers")
@RequirePermission(menu = "AWARD", perm = "CREATE")
@DataChangeLog(menu = "AWARD", table = "award_ledger")
public ApiResponse<Long> createLedger(@Valid @RequestBody AwardLedgerSaveReq req) {
return ApiResponse.ok(service.createLedger(req));
}
}
@@ -0,0 +1,82 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.BranchAllowanceService;
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/branch-allowances")
@RequiredArgsConstructor
public class BranchAllowanceController {
private final BranchAllowanceService service;
// ── Rule ──────────────────────────────────────────────────────────────────
@GetMapping("/rules")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ")
public ApiResponse<PageResponse<BranchMgrAllowanceRuleResp>> listRules(
@ModelAttribute BranchMgrAllowanceRuleSearchParam param) {
return ApiResponse.ok(service.listRules(param));
}
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ")
public ApiResponse<BranchMgrAllowanceRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
@PostMapping("/rules")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "CREATE")
@DataChangeLog(menu = "BRANCH_MGR_ALLOWANCE", table = "branch_mgr_allowance_rule")
public ApiResponse<Long> createRule(@Valid @RequestBody BranchMgrAllowanceRuleSaveReq req) {
return ApiResponse.ok(service.createRule(req));
}
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "UPDATE")
@DataChangeLog(menu = "BRANCH_MGR_ALLOWANCE", table = "branch_mgr_allowance_rule")
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
@Valid @RequestBody BranchMgrAllowanceRuleSaveReq req) {
service.updateRule(ruleId, req);
return ApiResponse.ok();
}
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "DELETE")
@DataChangeLog(menu = "BRANCH_MGR_ALLOWANCE", table = "branch_mgr_allowance_rule")
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
service.deleteRule(ruleId);
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
@GetMapping("/ledgers")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ")
public ApiResponse<PageResponse<BranchMgrAllowanceLedgerResp>> listLedgers(
@ModelAttribute BranchMgrAllowanceLedgerSearchParam param) {
return ApiResponse.ok(service.listLedgers(param));
}
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ")
public ApiResponse<BranchMgrAllowanceLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
@PostMapping("/ledgers")
@RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "CREATE")
@DataChangeLog(menu = "BRANCH_MGR_ALLOWANCE", table = "branch_mgr_allowance_ledger")
public ApiResponse<Long> createLedger(@Valid @RequestBody BranchMgrAllowanceLedgerSaveReq req) {
return ApiResponse.ok(service.createLedger(req));
}
}
@@ -0,0 +1,43 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.CommissionSimulationService;
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.CommissionSimulationResp;
import com.ga.core.vo.commission.CommissionSimulationSaveReq;
import com.ga.core.vo.commission.CommissionSimulationSearchParam;
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/commission-simulations")
@RequiredArgsConstructor
public class CommissionSimulationController {
private final CommissionSimulationService service;
@GetMapping
@RequirePermission(menu = "COMM_SIMULATOR", perm = "READ")
public ApiResponse<PageResponse<CommissionSimulationResp>> list(
@ModelAttribute CommissionSimulationSearchParam param) {
return ApiResponse.ok(service.list(param));
}
@GetMapping("/{simId}")
@RequirePermission(menu = "COMM_SIMULATOR", perm = "READ")
public ApiResponse<CommissionSimulationResp> detail(@PathVariable Long simId) {
return ApiResponse.ok(service.detail(simId));
}
@PostMapping("/simulate")
@RequirePermission(menu = "COMM_SIMULATOR", perm = "CREATE")
@DataChangeLog(menu = "COMM_SIMULATOR", table = "commission_simulation")
public ApiResponse<CommissionSimulationResp> simulate(@Valid @RequestBody CommissionSimulationSaveReq req) {
return ApiResponse.ok(service.simulate(req));
}
}
@@ -0,0 +1,102 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.GradeEvaluationService;
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.AgentGradeEvaluationResp;
import com.ga.core.vo.commission.AgentGradeEvaluationSaveReq;
import com.ga.core.vo.commission.AgentGradeEvaluationSearchParam;
import com.ga.core.vo.commission.GradeEvalRuleResp;
import com.ga.core.vo.commission.GradeEvalRuleSaveReq;
import com.ga.core.vo.commission.GradeEvalRuleSearchParam;
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/grade-evaluations")
@RequiredArgsConstructor
public class GradeEvaluationController {
private final GradeEvaluationService service;
// ── Rule ──────────────────────────────────────────────────────────────────
@GetMapping("/rules")
@RequirePermission(menu = "GRADE_EVAL", perm = "READ")
public ApiResponse<PageResponse<GradeEvalRuleResp>> listRules(@ModelAttribute GradeEvalRuleSearchParam param) {
return ApiResponse.ok(service.listRules(param));
}
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "GRADE_EVAL", perm = "READ")
public ApiResponse<GradeEvalRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
@PostMapping("/rules")
@RequirePermission(menu = "GRADE_EVAL", perm = "CREATE")
@DataChangeLog(menu = "GRADE_EVAL", table = "grade_eval_rule")
public ApiResponse<Long> createRule(@Valid @RequestBody GradeEvalRuleSaveReq req) {
return ApiResponse.ok(service.createRule(req));
}
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "GRADE_EVAL", perm = "UPDATE")
@DataChangeLog(menu = "GRADE_EVAL", table = "grade_eval_rule")
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
@Valid @RequestBody GradeEvalRuleSaveReq req) {
service.updateRule(ruleId, req);
return ApiResponse.ok();
}
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "GRADE_EVAL", perm = "DELETE")
@DataChangeLog(menu = "GRADE_EVAL", table = "grade_eval_rule")
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
service.deleteRule(ruleId);
return ApiResponse.ok();
}
// ── Evaluation ────────────────────────────────────────────────────────────
@GetMapping
@RequirePermission(menu = "GRADE_EVAL", perm = "READ")
public ApiResponse<PageResponse<AgentGradeEvaluationResp>> listEvals(
@ModelAttribute AgentGradeEvaluationSearchParam param) {
return ApiResponse.ok(service.listEvals(param));
}
@GetMapping("/{evalId}")
@RequirePermission(menu = "GRADE_EVAL", perm = "READ")
public ApiResponse<AgentGradeEvaluationResp> detailEval(@PathVariable Long evalId) {
return ApiResponse.ok(service.detailEval(evalId));
}
@PostMapping
@RequirePermission(menu = "GRADE_EVAL", perm = "CREATE")
@DataChangeLog(menu = "GRADE_EVAL", table = "agent_grade_evaluation")
public ApiResponse<Long> createEval(@Valid @RequestBody AgentGradeEvaluationSaveReq req) {
return ApiResponse.ok(service.createEval(req));
}
@PostMapping("/{evalId}/apply")
@RequirePermission(menu = "GRADE_EVAL", perm = "UPDATE")
@DataChangeLog(menu = "GRADE_EVAL", table = "agent_grade_evaluation")
public ApiResponse<Void> applyEval(@PathVariable Long evalId) {
service.applyEval(evalId);
return ApiResponse.ok();
}
@DeleteMapping("/{evalId}")
@RequirePermission(menu = "GRADE_EVAL", perm = "DELETE")
@DataChangeLog(menu = "GRADE_EVAL", table = "agent_grade_evaluation")
public ApiResponse<Void> deleteEval(@PathVariable Long evalId) {
service.deleteEval(evalId);
return ApiResponse.ok();
}
}
@@ -0,0 +1,60 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.GuaranteeInsuranceService;
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.AgentGuaranteeInsuranceResp;
import com.ga.core.vo.commission.AgentGuaranteeInsuranceSaveReq;
import com.ga.core.vo.commission.AgentGuaranteeInsuranceSearchParam;
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/guarantee-insurances")
@RequiredArgsConstructor
public class GuaranteeInsuranceController {
private final GuaranteeInsuranceService service;
@GetMapping
@RequirePermission(menu = "GUARANTEE_INS", perm = "READ")
public ApiResponse<PageResponse<AgentGuaranteeInsuranceResp>> list(
@ModelAttribute AgentGuaranteeInsuranceSearchParam param) {
return ApiResponse.ok(service.list(param));
}
@GetMapping("/{guaranteeId}")
@RequirePermission(menu = "GUARANTEE_INS", perm = "READ")
public ApiResponse<AgentGuaranteeInsuranceResp> detail(@PathVariable Long guaranteeId) {
return ApiResponse.ok(service.detail(guaranteeId));
}
@PostMapping
@RequirePermission(menu = "GUARANTEE_INS", perm = "CREATE")
@DataChangeLog(menu = "GUARANTEE_INS", table = "agent_guarantee_insurance")
public ApiResponse<Long> create(@Valid @RequestBody AgentGuaranteeInsuranceSaveReq req) {
return ApiResponse.ok(service.create(req));
}
@PutMapping("/{guaranteeId}")
@RequirePermission(menu = "GUARANTEE_INS", perm = "UPDATE")
@DataChangeLog(menu = "GUARANTEE_INS", table = "agent_guarantee_insurance")
public ApiResponse<Void> update(@PathVariable Long guaranteeId,
@Valid @RequestBody AgentGuaranteeInsuranceSaveReq req) {
service.update(guaranteeId, req);
return ApiResponse.ok();
}
@DeleteMapping("/{guaranteeId}")
@RequirePermission(menu = "GUARANTEE_INS", perm = "DELETE")
@DataChangeLog(menu = "GUARANTEE_INS", table = "agent_guarantee_insurance")
public ApiResponse<Void> delete(@PathVariable Long guaranteeId) {
service.delete(guaranteeId);
return ApiResponse.ok();
}
}
@@ -0,0 +1,66 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.HqSettleService;
import com.ga.api.service.commission.HqSettleService.HqSummaryResp;
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.HqSettleItemResp;
import com.ga.core.vo.commission.HqSettleItemSaveReq;
import com.ga.core.vo.commission.HqSettleItemSearchParam;
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/hq-settles")
@RequiredArgsConstructor
public class HqSettleController {
private final HqSettleService service;
@GetMapping
@RequirePermission(menu = "HQ_SETTLE", perm = "READ")
public ApiResponse<PageResponse<HqSettleItemResp>> list(@ModelAttribute HqSettleItemSearchParam param) {
return ApiResponse.ok(service.list(param));
}
@GetMapping("/summary")
@RequirePermission(menu = "HQ_SETTLE", perm = "READ")
public ApiResponse<HqSummaryResp> summary(@RequestParam String settleMonth) {
return ApiResponse.ok(service.summary(settleMonth));
}
@GetMapping("/{itemId}")
@RequirePermission(menu = "HQ_SETTLE", perm = "READ")
public ApiResponse<HqSettleItemResp> detail(@PathVariable Long itemId) {
return ApiResponse.ok(service.detail(itemId));
}
@PostMapping
@RequirePermission(menu = "HQ_SETTLE", perm = "CREATE")
@DataChangeLog(menu = "HQ_SETTLE", table = "hq_settle_item")
public ApiResponse<Long> create(@Valid @RequestBody HqSettleItemSaveReq req) {
return ApiResponse.ok(service.create(req));
}
@PutMapping("/{itemId}")
@RequirePermission(menu = "HQ_SETTLE", perm = "UPDATE")
@DataChangeLog(menu = "HQ_SETTLE", table = "hq_settle_item")
public ApiResponse<Void> update(@PathVariable Long itemId,
@Valid @RequestBody HqSettleItemSaveReq req) {
service.update(itemId, req);
return ApiResponse.ok();
}
@DeleteMapping("/{itemId}")
@RequirePermission(menu = "HQ_SETTLE", perm = "DELETE")
@DataChangeLog(menu = "HQ_SETTLE", table = "hq_settle_item")
public ApiResponse<Void> delete(@PathVariable Long itemId) {
service.delete(itemId);
return ApiResponse.ok();
}
}
@@ -0,0 +1,66 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.IncomeCommissionService;
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/income-commissions")
@RequiredArgsConstructor
public class IncomeCommissionController {
private final IncomeCommissionService service;
// ── Income Ledger ────────────────────────────────────────────────────────
@GetMapping("/ledgers")
@RequirePermission(menu = "INCOME_COMM", perm = "READ")
public ApiResponse<PageResponse<IncomeCommissionLedgerResp>> listLedgers(
@ModelAttribute IncomeCommissionLedgerSearchParam param) {
return ApiResponse.ok(service.listLedgers(param));
}
@GetMapping("/ledgers/{incomeId}")
@RequirePermission(menu = "INCOME_COMM", perm = "READ")
public ApiResponse<IncomeCommissionLedgerResp> detailLedger(@PathVariable Long incomeId) {
return ApiResponse.ok(service.detailLedger(incomeId));
}
@PostMapping("/ledgers")
@RequirePermission(menu = "INCOME_COMM", perm = "CREATE")
@DataChangeLog(menu = "INCOME_COMM", table = "income_commission_ledger")
public ApiResponse<Long> createLedger(@Valid @RequestBody IncomeCommissionLedgerSaveReq req) {
return ApiResponse.ok(service.createLedger(req));
}
// ── Margin Summary ───────────────────────────────────────────────────────
@GetMapping("/margins")
@RequirePermission(menu = "INCOME_COMM", perm = "READ")
public ApiResponse<PageResponse<MarginSummaryResp>> listMargins(
@ModelAttribute MarginSummarySearchParam param) {
return ApiResponse.ok(service.listMargins(param));
}
@GetMapping("/margins/{marginId}")
@RequirePermission(menu = "INCOME_COMM", perm = "READ")
public ApiResponse<MarginSummaryResp> detailMargin(@PathVariable Long marginId) {
return ApiResponse.ok(service.detailMargin(marginId));
}
@PostMapping("/margin/recalc")
@RequirePermission(menu = "INCOME_COMM", perm = "CREATE")
@DataChangeLog(menu = "INCOME_COMM", table = "margin_summary")
public ApiResponse<Long> recalcMargin(@RequestParam String settleMonth,
@RequestParam(required = false) Long agentId) {
return ApiResponse.ok(service.recalcMargin(settleMonth, agentId));
}
}
@@ -0,0 +1,59 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.JointContractService;
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.JointContractResp;
import com.ga.core.vo.commission.JointContractSaveReq;
import com.ga.core.vo.commission.JointContractSearchParam;
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/joint-contracts")
@RequiredArgsConstructor
public class JointContractController {
private final JointContractService service;
@GetMapping
@RequirePermission(menu = "JOINT_CONTRACT", perm = "READ")
public ApiResponse<PageResponse<JointContractResp>> list(@ModelAttribute JointContractSearchParam param) {
return ApiResponse.ok(service.list(param));
}
@GetMapping("/{jointId}")
@RequirePermission(menu = "JOINT_CONTRACT", perm = "READ")
public ApiResponse<JointContractResp> detail(@PathVariable Long jointId) {
return ApiResponse.ok(service.detail(jointId));
}
@PostMapping
@RequirePermission(menu = "JOINT_CONTRACT", perm = "CREATE")
@DataChangeLog(menu = "JOINT_CONTRACT", table = "joint_contract")
public ApiResponse<Long> create(@Valid @RequestBody JointContractSaveReq req) {
return ApiResponse.ok(service.create(req));
}
@PutMapping("/{jointId}")
@RequirePermission(menu = "JOINT_CONTRACT", perm = "UPDATE")
@DataChangeLog(menu = "JOINT_CONTRACT", table = "joint_contract")
public ApiResponse<Void> update(@PathVariable Long jointId,
@Valid @RequestBody JointContractSaveReq req) {
service.update(jointId, req);
return ApiResponse.ok();
}
@DeleteMapping("/{jointId}")
@RequirePermission(menu = "JOINT_CONTRACT", perm = "DELETE")
@DataChangeLog(menu = "JOINT_CONTRACT", table = "joint_contract")
public ApiResponse<Void> delete(@PathVariable Long jointId) {
service.delete(jointId);
return ApiResponse.ok();
}
}
@@ -0,0 +1,82 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.LifeSupportService;
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/life-supports")
@RequiredArgsConstructor
public class LifeSupportController {
private final LifeSupportService service;
// ── Rule ──────────────────────────────────────────────────────────────────
@GetMapping("/rules")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ")
public ApiResponse<PageResponse<LifeSupportRuleResp>> listRules(
@ModelAttribute LifeSupportRuleSearchParam param) {
return ApiResponse.ok(service.listRules(param));
}
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ")
public ApiResponse<LifeSupportRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
@PostMapping("/rules")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "CREATE")
@DataChangeLog(menu = "LIFE_OP_SUPPORT", table = "life_support_rule")
public ApiResponse<Long> createRule(@Valid @RequestBody LifeSupportRuleSaveReq req) {
return ApiResponse.ok(service.createRule(req));
}
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "UPDATE")
@DataChangeLog(menu = "LIFE_OP_SUPPORT", table = "life_support_rule")
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
@Valid @RequestBody LifeSupportRuleSaveReq req) {
service.updateRule(ruleId, req);
return ApiResponse.ok();
}
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "DELETE")
@DataChangeLog(menu = "LIFE_OP_SUPPORT", table = "life_support_rule")
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
service.deleteRule(ruleId);
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
@GetMapping("/ledgers")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ")
public ApiResponse<PageResponse<LifeSupportLedgerResp>> listLedgers(
@ModelAttribute LifeSupportLedgerSearchParam param) {
return ApiResponse.ok(service.listLedgers(param));
}
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ")
public ApiResponse<LifeSupportLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
@PostMapping("/ledgers")
@RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "CREATE")
@DataChangeLog(menu = "LIFE_OP_SUPPORT", table = "life_support_ledger")
public ApiResponse<Long> createLedger(@Valid @RequestBody LifeSupportLedgerSaveReq req) {
return ApiResponse.ok(service.createLedger(req));
}
}
@@ -0,0 +1,82 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.RecognizedPerformanceService;
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/recognized-performances")
@RequiredArgsConstructor
public class RecognizedPerformanceController {
private final RecognizedPerformanceService service;
// ── Product Conversion Rate (rule) ────────────────────────────────────────
@GetMapping("/rates")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ")
public ApiResponse<PageResponse<ProductConversionRateResp>> listRates(
@ModelAttribute ProductConversionRateSearchParam param) {
return ApiResponse.ok(service.listRates(param));
}
@GetMapping("/rates/{rateId}")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ")
public ApiResponse<ProductConversionRateResp> detailRate(@PathVariable Long rateId) {
return ApiResponse.ok(service.detailRate(rateId));
}
@PostMapping("/rates")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "CREATE")
@DataChangeLog(menu = "RECOGNIZED_PERF", table = "product_conversion_rate")
public ApiResponse<Long> createRate(@Valid @RequestBody ProductConversionRateSaveReq req) {
return ApiResponse.ok(service.createRate(req));
}
@PutMapping("/rates/{rateId}")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "UPDATE")
@DataChangeLog(menu = "RECOGNIZED_PERF", table = "product_conversion_rate")
public ApiResponse<Void> updateRate(@PathVariable Long rateId,
@Valid @RequestBody ProductConversionRateSaveReq req) {
service.updateRate(rateId, req);
return ApiResponse.ok();
}
@DeleteMapping("/rates/{rateId}")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "DELETE")
@DataChangeLog(menu = "RECOGNIZED_PERF", table = "product_conversion_rate")
public ApiResponse<Void> deleteRate(@PathVariable Long rateId) {
service.deleteRate(rateId);
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
@GetMapping("/ledgers")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ")
public ApiResponse<PageResponse<RecognizedPerformanceLedgerResp>> listLedgers(
@ModelAttribute RecognizedPerformanceLedgerSearchParam param) {
return ApiResponse.ok(service.listLedgers(param));
}
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ")
public ApiResponse<RecognizedPerformanceLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
@PostMapping("/ledgers")
@RequirePermission(menu = "RECOGNIZED_PERF", perm = "CREATE")
@DataChangeLog(menu = "RECOGNIZED_PERF", table = "recognized_performance_ledger")
public ApiResponse<Long> createLedger(@Valid @RequestBody RecognizedPerformanceLedgerSaveReq req) {
return ApiResponse.ok(service.createLedger(req));
}
}
@@ -0,0 +1,82 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.ReferralCommissionService;
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/referral-commissions")
@RequiredArgsConstructor
public class ReferralCommissionController {
private final ReferralCommissionService service;
// ── Rule ──────────────────────────────────────────────────────────────────
@GetMapping("/rules")
@RequirePermission(menu = "REFERRAL_COMM", perm = "READ")
public ApiResponse<PageResponse<ReferralCommissionRuleResp>> listRules(
@ModelAttribute ReferralCommissionRuleSearchParam param) {
return ApiResponse.ok(service.listRules(param));
}
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "REFERRAL_COMM", perm = "READ")
public ApiResponse<ReferralCommissionRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
@PostMapping("/rules")
@RequirePermission(menu = "REFERRAL_COMM", perm = "CREATE")
@DataChangeLog(menu = "REFERRAL_COMM", table = "referral_commission_rule")
public ApiResponse<Long> createRule(@Valid @RequestBody ReferralCommissionRuleSaveReq req) {
return ApiResponse.ok(service.createRule(req));
}
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "REFERRAL_COMM", perm = "UPDATE")
@DataChangeLog(menu = "REFERRAL_COMM", table = "referral_commission_rule")
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
@Valid @RequestBody ReferralCommissionRuleSaveReq req) {
service.updateRule(ruleId, req);
return ApiResponse.ok();
}
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "REFERRAL_COMM", perm = "DELETE")
@DataChangeLog(menu = "REFERRAL_COMM", table = "referral_commission_rule")
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
service.deleteRule(ruleId);
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
@GetMapping("/ledgers")
@RequirePermission(menu = "REFERRAL_COMM", perm = "READ")
public ApiResponse<PageResponse<ReferralCommissionLedgerResp>> listLedgers(
@ModelAttribute ReferralCommissionLedgerSearchParam param) {
return ApiResponse.ok(service.listLedgers(param));
}
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "REFERRAL_COMM", perm = "READ")
public ApiResponse<ReferralCommissionLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
@PostMapping("/ledgers")
@RequirePermission(menu = "REFERRAL_COMM", perm = "CREATE")
@DataChangeLog(menu = "REFERRAL_COMM", table = "referral_commission_ledger")
public ApiResponse<Long> createLedger(@Valid @RequestBody ReferralCommissionLedgerSaveReq req) {
return ApiResponse.ok(service.createLedger(req));
}
}
@@ -0,0 +1,42 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.ReserveFundService;
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.AgentReserveFundResp;
import com.ga.core.vo.commission.AgentReserveFundSaveReq;
import com.ga.core.vo.commission.AgentReserveFundSearchParam;
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/reserve-funds")
@RequiredArgsConstructor
public class ReserveFundController {
private final ReserveFundService service;
@GetMapping
@RequirePermission(menu = "RESERVE_FUND", perm = "READ")
public ApiResponse<PageResponse<AgentReserveFundResp>> list(@ModelAttribute AgentReserveFundSearchParam param) {
return ApiResponse.ok(service.list(param));
}
@GetMapping("/{reserveId}")
@RequirePermission(menu = "RESERVE_FUND", perm = "READ")
public ApiResponse<AgentReserveFundResp> detail(@PathVariable Long reserveId) {
return ApiResponse.ok(service.detail(reserveId));
}
@PostMapping
@RequirePermission(menu = "RESERVE_FUND", perm = "CREATE")
@DataChangeLog(menu = "RESERVE_FUND", table = "agent_reserve_fund")
public ApiResponse<Long> create(@Valid @RequestBody AgentReserveFundSaveReq req) {
return ApiResponse.ok(service.create(req));
}
}
@@ -0,0 +1,82 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.SettlingSupportService;
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/settling-supports")
@RequiredArgsConstructor
public class SettlingSupportController {
private final SettlingSupportService service;
// ── Rule ──────────────────────────────────────────────────────────────────
@GetMapping("/rules")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ")
public ApiResponse<PageResponse<SettlingSupportRuleResp>> listRules(
@ModelAttribute SettlingSupportRuleSearchParam param) {
return ApiResponse.ok(service.listRules(param));
}
@GetMapping("/rules/{ruleId}")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ")
public ApiResponse<SettlingSupportRuleResp> detailRule(@PathVariable Long ruleId) {
return ApiResponse.ok(service.detailRule(ruleId));
}
@PostMapping("/rules")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "CREATE")
@DataChangeLog(menu = "SETTLING_SUPPORT", table = "settling_support_rule")
public ApiResponse<Long> createRule(@Valid @RequestBody SettlingSupportRuleSaveReq req) {
return ApiResponse.ok(service.createRule(req));
}
@PutMapping("/rules/{ruleId}")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "UPDATE")
@DataChangeLog(menu = "SETTLING_SUPPORT", table = "settling_support_rule")
public ApiResponse<Void> updateRule(@PathVariable Long ruleId,
@Valid @RequestBody SettlingSupportRuleSaveReq req) {
service.updateRule(ruleId, req);
return ApiResponse.ok();
}
@DeleteMapping("/rules/{ruleId}")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "DELETE")
@DataChangeLog(menu = "SETTLING_SUPPORT", table = "settling_support_rule")
public ApiResponse<Void> deleteRule(@PathVariable Long ruleId) {
service.deleteRule(ruleId);
return ApiResponse.ok();
}
// ── Ledger ────────────────────────────────────────────────────────────────
@GetMapping("/ledgers")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ")
public ApiResponse<PageResponse<SettlingSupportLedgerResp>> listLedgers(
@ModelAttribute SettlingSupportLedgerSearchParam param) {
return ApiResponse.ok(service.listLedgers(param));
}
@GetMapping("/ledgers/{ledgerId}")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ")
public ApiResponse<SettlingSupportLedgerResp> detailLedger(@PathVariable Long ledgerId) {
return ApiResponse.ok(service.detailLedger(ledgerId));
}
@PostMapping("/ledgers")
@RequirePermission(menu = "SETTLING_SUPPORT", perm = "CREATE")
@DataChangeLog(menu = "SETTLING_SUPPORT", table = "settling_support_ledger")
public ApiResponse<Long> createLedger(@Valid @RequestBody SettlingSupportLedgerSaveReq req) {
return ApiResponse.ok(service.createLedger(req));
}
}
@@ -0,0 +1,66 @@
package com.ga.api.controller.commission;
import com.ga.api.service.commission.StepCloseService;
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.StepCloseResp;
import com.ga.core.vo.commission.StepCloseSaveReq;
import com.ga.core.vo.commission.StepCloseSearchParam;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "단계마감")
@RestController
@RequestMapping("/api/step-closes")
@RequiredArgsConstructor
public class StepCloseController {
private final StepCloseService service;
@GetMapping
@RequirePermission(menu = "STEP_CLOSE", perm = "READ")
public ApiResponse<PageResponse<StepCloseResp>> list(@ModelAttribute StepCloseSearchParam param) {
return ApiResponse.ok(service.list(param));
}
@GetMapping("/by-month")
@RequirePermission(menu = "STEP_CLOSE", perm = "READ")
public ApiResponse<List<StepCloseResp>> listByMonth(@RequestParam String settleMonth) {
return ApiResponse.ok(service.listByMonth(settleMonth));
}
@GetMapping("/{stepCloseId}")
@RequirePermission(menu = "STEP_CLOSE", perm = "READ")
public ApiResponse<StepCloseResp> detail(@PathVariable Long stepCloseId) {
return ApiResponse.ok(service.detail(stepCloseId));
}
@PostMapping
@RequirePermission(menu = "STEP_CLOSE", perm = "UPDATE")
@DataChangeLog(menu = "STEP_CLOSE", table = "settlement_step_close")
public ApiResponse<Long> create(@Valid @RequestBody StepCloseSaveReq req) {
return ApiResponse.ok(service.create(req));
}
@PostMapping("/{stepCloseId}/close")
@RequirePermission(menu = "STEP_CLOSE", perm = "APPROVE")
@DataChangeLog(menu = "STEP_CLOSE", table = "settlement_step_close")
public ApiResponse<Void> close(@PathVariable Long stepCloseId) {
service.close(stepCloseId);
return ApiResponse.ok();
}
@PostMapping("/{stepCloseId}/reopen")
@RequirePermission(menu = "STEP_CLOSE", perm = "APPROVE")
@DataChangeLog(menu = "STEP_CLOSE", table = "settlement_step_close")
public ApiResponse<Void> reopen(@PathVariable Long stepCloseId) {
service.reopen(stepCloseId);
return ApiResponse.ok();
}
}
@@ -0,0 +1,114 @@
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.AwardLedgerMapper;
import com.ga.core.mapper.commission.AwardRuleMapper;
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.time.LocalDate;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class AwardService {
private final AwardRuleMapper ruleMapper;
private final AwardLedgerMapper ledgerMapper;
// ── Rule ──────────────────────────────────────────────────────────────────
public PageResponse<AwardRuleResp> listRules(AwardRuleSearchParam param) {
param.startPage();
return PageResponse.of(ruleMapper.selectList(param));
}
public AwardRuleResp detailRule(Long ruleId) {
AwardRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createRule(AwardRuleSaveReq req) {
AwardRuleVO vo = new AwardRuleVO();
vo.setAwardType(req.getAwardType());
vo.setCalcType(req.getCalcType());
vo.setThresholdAmount(req.getThresholdAmount());
vo.setAwardRate(req.getAwardRate());
vo.setAwardFixed(req.getAwardFixed());
vo.setEffectiveFrom(req.getEffectiveFrom());
vo.setEffectiveTo(req.getEffectiveTo());
ruleMapper.insert(vo);
return vo.getRuleId();
}
@Transactional
public void updateRule(Long ruleId, AwardRuleSaveReq req) {
AwardRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
AwardRuleVO vo = new AwardRuleVO();
vo.setRuleId(ruleId);
vo.setAwardType(req.getAwardType());
vo.setCalcType(req.getCalcType());
vo.setThresholdAmount(req.getThresholdAmount());
vo.setAwardRate(req.getAwardRate());
vo.setAwardFixed(req.getAwardFixed());
vo.setEffectiveFrom(req.getEffectiveFrom());
vo.setEffectiveTo(req.getEffectiveTo());
ruleMapper.update(vo);
}
@Transactional
public void deleteRule(Long ruleId) {
AwardRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
ruleMapper.delete(ruleId);
}
// ── Ledger ────────────────────────────────────────────────────────────────
public PageResponse<AwardLedgerResp> listLedgers(AwardLedgerSearchParam param) {
param.startPage();
return PageResponse.of(ledgerMapper.selectList(param));
}
public AwardLedgerResp detailLedger(Long ledgerId) {
AwardLedgerResp resp = ledgerMapper.selectById(ledgerId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createLedger(AwardLedgerSaveReq req) {
// 룰 조회로 calc_type 결정
AwardRuleVO rule = ruleMapper.selectActiveRule(req.getAwardType(), LocalDate.now());
if (rule == null) throw new BizException(ErrorCode.NOT_FOUND);
BigDecimal awardAmount;
if ("FIXED".equals(rule.getCalcType())) {
awardAmount = rule.getAwardFixed().setScale(2, RoundingMode.HALF_UP);
} else {
// RATE: base × award_rate
awardAmount = req.getBaseAmount()
.multiply(rule.getAwardRate())
.setScale(2, RoundingMode.HALF_UP);
}
AwardLedgerVO vo = new AwardLedgerVO();
vo.setAgentId(req.getAgentId());
vo.setSettleMonth(req.getSettleMonth());
vo.setAwardType(req.getAwardType());
vo.setBaseAmount(req.getBaseAmount());
vo.setAwardAmount(awardAmount);
vo.setStatus("CALCULATED");
ledgerMapper.insert(vo);
return vo.getLedgerId();
}
}
@@ -0,0 +1,107 @@
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.BranchMgrAllowanceLedgerMapper;
import com.ga.core.mapper.commission.BranchMgrAllowanceRuleMapper;
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;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BranchAllowanceService {
private final BranchMgrAllowanceRuleMapper ruleMapper;
private final BranchMgrAllowanceLedgerMapper ledgerMapper;
// ── Rule ──────────────────────────────────────────────────────────────────
public PageResponse<BranchMgrAllowanceRuleResp> listRules(BranchMgrAllowanceRuleSearchParam param) {
param.startPage();
return PageResponse.of(ruleMapper.selectList(param));
}
public BranchMgrAllowanceRuleResp detailRule(Long ruleId) {
BranchMgrAllowanceRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createRule(BranchMgrAllowanceRuleSaveReq req) {
BranchMgrAllowanceRuleVO vo = new BranchMgrAllowanceRuleVO();
vo.setPositionCode(req.getPositionCode());
vo.setAllowanceRate(req.getAllowanceRate());
vo.setCapAmount(req.getCapAmount());
vo.setEffectiveFrom(req.getEffectiveFrom());
vo.setEffectiveTo(req.getEffectiveTo());
ruleMapper.insert(vo);
return vo.getRuleId();
}
@Transactional
public void updateRule(Long ruleId, BranchMgrAllowanceRuleSaveReq req) {
BranchMgrAllowanceRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
BranchMgrAllowanceRuleVO vo = new BranchMgrAllowanceRuleVO();
vo.setRuleId(ruleId);
vo.setPositionCode(req.getPositionCode());
vo.setAllowanceRate(req.getAllowanceRate());
vo.setCapAmount(req.getCapAmount());
vo.setEffectiveFrom(req.getEffectiveFrom());
vo.setEffectiveTo(req.getEffectiveTo());
ruleMapper.update(vo);
}
@Transactional
public void deleteRule(Long ruleId) {
BranchMgrAllowanceRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
ruleMapper.delete(ruleId);
}
// ── Ledger ────────────────────────────────────────────────────────────────
public PageResponse<BranchMgrAllowanceLedgerResp> listLedgers(BranchMgrAllowanceLedgerSearchParam param) {
param.startPage();
return PageResponse.of(ledgerMapper.selectList(param));
}
public BranchMgrAllowanceLedgerResp detailLedger(Long ledgerId) {
BranchMgrAllowanceLedgerResp resp = ledgerMapper.selectById(ledgerId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createLedger(BranchMgrAllowanceLedgerSaveReq req) {
// 수당액 = min(base × rate, cap) — cap NULL이면 상한 없음
BigDecimal raw = req.getBaseAmount()
.multiply(req.getAllowanceRate())
.setScale(2, RoundingMode.HALF_UP);
BigDecimal clawback = req.getClawbackAmount() != null ? req.getClawbackAmount() : BigDecimal.ZERO;
// cap_amount는 SaveReq에 없으므로 allowanceRate와 동일 룰에서 cap 조회
// positionCode가 req에 없어 단순 구현: raw 그대로 사용 (배치에서 룰 참조 시 위임)
BigDecimal allowanceAmount = raw;
BranchMgrAllowanceLedgerVO vo = new BranchMgrAllowanceLedgerVO();
vo.setManagerAgentId(req.getManagerAgentId());
vo.setOrgId(req.getOrgId());
vo.setSettleMonth(req.getSettleMonth());
vo.setBaseAmount(req.getBaseAmount());
vo.setAllowanceRate(req.getAllowanceRate());
vo.setAllowanceAmount(allowanceAmount);
vo.setClawbackAmount(clawback);
vo.setStatus("CALCULATED");
ledgerMapper.insert(vo);
return vo.getLedgerId();
}
}
@@ -0,0 +1,94 @@
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.CommissionSimulationMapper;
import com.ga.core.vo.commission.CommissionSimulationResp;
import com.ga.core.vo.commission.CommissionSimulationSaveReq;
import com.ga.core.vo.commission.CommissionSimulationSearchParam;
import com.ga.core.vo.commission.CommissionSimulationVO;
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)
public class CommissionSimulationService {
private static final BigDecimal WITHHOLDING_TAX_RATE = new BigDecimal("0.033");
private static final BigDecimal DEFAULT_CHARGEBACK_RATE = new BigDecimal("0.5");
private final CommissionSimulationMapper simulationMapper;
public PageResponse<CommissionSimulationResp> list(CommissionSimulationSearchParam param) {
param.startPage();
return PageResponse.of(simulationMapper.selectList(param));
}
public CommissionSimulationResp detail(Long simId) {
CommissionSimulationResp resp = simulationMapper.selectById(simId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
/**
* 수수료 시뮬레이션 계산 후 저장·반환.
* company_payment = premium × company_rate
* agent_commission = company_payment × payout_rate
* withholding_tax = agent_commission × 3.3%
* net_amount = agent_commission withholding_tax
* expected_chargeback: 입력 값이 있으면 사용, 없으면 agent_commission × 50% (13개월 가정)
* 모든 금액 setScale(2, HALF_UP).
*/
@Transactional
public CommissionSimulationResp simulate(CommissionSimulationSaveReq req) {
BigDecimal companyPayment = req.getPremiumAmount()
.multiply(req.getCompanyRate())
.setScale(2, RoundingMode.HALF_UP);
BigDecimal agentCommission = companyPayment
.multiply(req.getPayoutRate())
.setScale(2, RoundingMode.HALF_UP);
BigDecimal withholdingTax = agentCommission
.multiply(WITHHOLDING_TAX_RATE)
.setScale(2, RoundingMode.HALF_UP);
BigDecimal netAmount = agentCommission
.subtract(withholdingTax)
.setScale(2, RoundingMode.HALF_UP);
BigDecimal expectedChargeback;
if (req.getExpectedChargeback() != null) {
expectedChargeback = req.getExpectedChargeback().setScale(2, RoundingMode.HALF_UP);
} else {
expectedChargeback = agentCommission
.multiply(DEFAULT_CHARGEBACK_RATE)
.setScale(2, RoundingMode.HALF_UP);
}
CommissionSimulationVO vo = new CommissionSimulationVO();
vo.setSimName(req.getSimName());
vo.setPremiumAmount(req.getPremiumAmount());
vo.setCompanyRate(req.getCompanyRate());
vo.setPayoutRate(req.getPayoutRate());
vo.setCommissionYear(req.getCommissionYear());
vo.setCompanyPayment(companyPayment);
vo.setAgentCommission(agentCommission);
vo.setWithholdingTax(withholdingTax);
vo.setNetAmount(netAmount);
vo.setExpectedChargeback(expectedChargeback);
vo.setCreatedBy(SecurityUtil.getCurrentUserId());
simulationMapper.insert(vo);
CommissionSimulationResp resp = simulationMapper.selectById(vo.getSimId());
if (resp == null) throw new BizException(ErrorCode.INTERNAL_ERROR);
return resp;
}
}
@@ -0,0 +1,129 @@
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.AgentGradeEvaluationMapper;
import com.ga.core.mapper.commission.GradeEvalRuleMapper;
import com.ga.core.vo.commission.AgentGradeEvaluationResp;
import com.ga.core.vo.commission.AgentGradeEvaluationSaveReq;
import com.ga.core.vo.commission.AgentGradeEvaluationSearchParam;
import com.ga.core.vo.commission.AgentGradeEvaluationVO;
import com.ga.core.vo.commission.GradeEvalRuleResp;
import com.ga.core.vo.commission.GradeEvalRuleSaveReq;
import com.ga.core.vo.commission.GradeEvalRuleSearchParam;
import com.ga.core.vo.commission.GradeEvalRuleVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDate;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class GradeEvaluationService {
private final GradeEvalRuleMapper ruleMapper;
private final AgentGradeEvaluationMapper evalMapper;
// ── Rule ──────────────────────────────────────────────────────────────────
public PageResponse<GradeEvalRuleResp> listRules(GradeEvalRuleSearchParam param) {
param.startPage();
return PageResponse.of(ruleMapper.selectList(param));
}
public GradeEvalRuleResp detailRule(Long ruleId) {
GradeEvalRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createRule(GradeEvalRuleSaveReq req) {
GradeEvalRuleVO vo = new GradeEvalRuleVO();
vo.setGradeId(req.getGradeId());
vo.setMinScore(req.getMinScore());
vo.setEffectiveFrom(req.getEffectiveFrom());
vo.setEffectiveTo(req.getEffectiveTo());
ruleMapper.insert(vo);
return vo.getRuleId();
}
@Transactional
public void updateRule(Long ruleId, GradeEvalRuleSaveReq req) {
GradeEvalRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
GradeEvalRuleVO vo = new GradeEvalRuleVO();
vo.setRuleId(ruleId);
vo.setGradeId(req.getGradeId());
vo.setMinScore(req.getMinScore());
vo.setEffectiveFrom(req.getEffectiveFrom());
vo.setEffectiveTo(req.getEffectiveTo());
ruleMapper.update(vo);
}
@Transactional
public void deleteRule(Long ruleId) {
GradeEvalRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
ruleMapper.delete(ruleId);
}
// ── Evaluation ────────────────────────────────────────────────────────────
public PageResponse<AgentGradeEvaluationResp> listEvals(AgentGradeEvaluationSearchParam param) {
param.startPage();
return PageResponse.of(evalMapper.selectList(param));
}
public AgentGradeEvaluationResp detailEval(Long evalId) {
AgentGradeEvaluationResp resp = evalMapper.selectById(evalId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
/**
* 등급평가 생성.
* selectActiveRuleByScore(score, 기준일)로 evaluated_grade_id 결정.
* 매칭 룰 없으면 prevGradeId 유지 또는 null.
*/
@Transactional
public Long createEval(AgentGradeEvaluationSaveReq req) {
LocalDate baseDate = LocalDate.now();
GradeEvalRuleVO matchedRule = ruleMapper.selectActiveRuleByScore(req.getPerformanceScore(), baseDate);
Long evaluatedGradeId = (matchedRule != null) ? matchedRule.getGradeId() : req.getPrevGradeId();
AgentGradeEvaluationVO vo = new AgentGradeEvaluationVO();
vo.setAgentId(req.getAgentId());
vo.setEvalPeriod(req.getEvalPeriod());
vo.setPerformanceScore(req.getPerformanceScore());
vo.setPrevGradeId(req.getPrevGradeId());
vo.setEvaluatedGradeId(evaluatedGradeId);
vo.setStatus("EVALUATED");
evalMapper.insert(vo);
return vo.getEvalId();
}
/**
* 등급평가 반영 액션 (EVALUATED → APPLIED).
*/
@Transactional
public void applyEval(Long evalId) {
AgentGradeEvaluationResp resp = evalMapper.selectById(evalId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
if ("APPLIED".equals(resp.getStatus())) {
throw new BizException(ErrorCode.INVALID_STATUS, "이미 반영된 평가입니다.");
}
evalMapper.updateStatus(evalId, "APPLIED");
}
@Transactional
public void deleteEval(Long evalId) {
AgentGradeEvaluationResp resp = evalMapper.selectById(evalId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
evalMapper.delete(evalId);
}
}
@@ -0,0 +1,68 @@
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.AgentGuaranteeInsuranceMapper;
import com.ga.core.vo.commission.AgentGuaranteeInsuranceResp;
import com.ga.core.vo.commission.AgentGuaranteeInsuranceSaveReq;
import com.ga.core.vo.commission.AgentGuaranteeInsuranceSearchParam;
import com.ga.core.vo.commission.AgentGuaranteeInsuranceVO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class GuaranteeInsuranceService {
private final AgentGuaranteeInsuranceMapper guaranteeInsuranceMapper;
public PageResponse<AgentGuaranteeInsuranceResp> list(AgentGuaranteeInsuranceSearchParam param) {
param.startPage();
return PageResponse.of(guaranteeInsuranceMapper.selectList(param));
}
public AgentGuaranteeInsuranceResp detail(Long guaranteeId) {
AgentGuaranteeInsuranceResp resp = guaranteeInsuranceMapper.selectById(guaranteeId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long create(AgentGuaranteeInsuranceSaveReq req) {
AgentGuaranteeInsuranceVO vo = new AgentGuaranteeInsuranceVO();
vo.setAgentId(req.getAgentId());
vo.setPolicyNo(req.getPolicyNo());
vo.setCarrierId(req.getCarrierId());
vo.setGuaranteeAmount(req.getGuaranteeAmount());
vo.setStartDate(req.getStartDate());
vo.setEndDate(req.getEndDate());
vo.setStatus("ACTIVE");
guaranteeInsuranceMapper.insert(vo);
return vo.getGuaranteeId();
}
@Transactional
public void update(Long guaranteeId, AgentGuaranteeInsuranceSaveReq req) {
AgentGuaranteeInsuranceResp resp = guaranteeInsuranceMapper.selectById(guaranteeId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
AgentGuaranteeInsuranceVO vo = new AgentGuaranteeInsuranceVO();
vo.setGuaranteeId(guaranteeId);
vo.setAgentId(req.getAgentId());
vo.setPolicyNo(req.getPolicyNo());
vo.setCarrierId(req.getCarrierId());
vo.setGuaranteeAmount(req.getGuaranteeAmount());
vo.setStartDate(req.getStartDate());
vo.setEndDate(req.getEndDate());
guaranteeInsuranceMapper.update(vo);
}
@Transactional
public void delete(Long guaranteeId) {
AgentGuaranteeInsuranceResp resp = guaranteeInsuranceMapper.selectById(guaranteeId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
guaranteeInsuranceMapper.delete(guaranteeId);
}
}
@@ -0,0 +1,90 @@
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.HqSettleItemMapper;
import com.ga.core.vo.commission.HqSettleItemResp;
import com.ga.core.vo.commission.HqSettleItemSaveReq;
import com.ga.core.vo.commission.HqSettleItemSearchParam;
import com.ga.core.vo.commission.HqSettleItemVO;
import lombok.Data;
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 HqSettleService {
private final HqSettleItemMapper itemMapper;
@Data
public static class HqSummaryResp {
private final String settleMonth;
private final BigDecimal income;
private final BigDecimal expense;
private final BigDecimal profit;
}
public PageResponse<HqSettleItemResp> list(HqSettleItemSearchParam param) {
param.startPage();
return PageResponse.of(itemMapper.selectList(param));
}
public HqSettleItemResp detail(Long itemId) {
HqSettleItemResp resp = itemMapper.selectById(itemId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
/**
* 본사손익 집계: income expense.
* 둘 다 null이면 0으로 처리.
*/
public HqSummaryResp summary(String settleMonth) {
BigDecimal income = itemMapper.sumByMonthAndType(settleMonth, "INCOME");
BigDecimal expense = itemMapper.sumByMonthAndType(settleMonth, "EXPENSE");
if (income == null) income = BigDecimal.ZERO;
if (expense == null) expense = BigDecimal.ZERO;
BigDecimal profit = income.subtract(expense);
return new HqSummaryResp(settleMonth, income, expense, profit);
}
@Transactional
public Long create(HqSettleItemSaveReq req) {
HqSettleItemVO vo = new HqSettleItemVO();
vo.setSettleMonth(req.getSettleMonth());
vo.setItemType(req.getItemType());
vo.setItemName(req.getItemName());
vo.setAmount(req.getAmount());
vo.setMemo(req.getMemo());
vo.setStatus("REGISTERED");
itemMapper.insert(vo);
return vo.getItemId();
}
@Transactional
public void update(Long itemId, HqSettleItemSaveReq req) {
HqSettleItemResp resp = itemMapper.selectById(itemId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
HqSettleItemVO vo = new HqSettleItemVO();
vo.setItemId(itemId);
vo.setSettleMonth(req.getSettleMonth());
vo.setItemType(req.getItemType());
vo.setItemName(req.getItemName());
vo.setAmount(req.getAmount());
vo.setMemo(req.getMemo());
itemMapper.update(vo);
}
@Transactional
public void delete(Long itemId) {
HqSettleItemResp resp = itemMapper.selectById(itemId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
itemMapper.delete(itemId);
}
}
@@ -0,0 +1,109 @@
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.IncomeCommissionLedgerMapper;
import com.ga.core.mapper.commission.MarginSummaryMapper;
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;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class IncomeCommissionService {
private final IncomeCommissionLedgerMapper ledgerMapper;
private final MarginSummaryMapper marginMapper;
// Income Ledger
public PageResponse<IncomeCommissionLedgerResp> listLedgers(IncomeCommissionLedgerSearchParam param) {
param.startPage();
return PageResponse.of(ledgerMapper.selectList(param));
}
public IncomeCommissionLedgerResp detailLedger(Long incomeId) {
IncomeCommissionLedgerResp resp = ledgerMapper.selectById(incomeId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createLedger(IncomeCommissionLedgerSaveReq req) {
IncomeCommissionLedgerVO vo = new IncomeCommissionLedgerVO();
vo.setContractId(req.getContractId());
vo.setCarrierId(req.getCarrierId());
vo.setSettleMonth(req.getSettleMonth());
vo.setPremiumAmount(req.getPremiumAmount());
vo.setCompanyRate(req.getCompanyRate());
// 수입수수료 = 보험료 × 보험사요율
vo.setIncomeAmount(req.getPremiumAmount()
.multiply(req.getCompanyRate())
.setScale(2, RoundingMode.HALF_UP));
vo.setCommissionType(req.getCommissionType());
vo.setStatus("CALCULATED");
ledgerMapper.insert(vo);
return vo.getIncomeId();
}
// Margin Summary
public PageResponse<MarginSummaryResp> listMargins(MarginSummarySearchParam param) {
param.startPage();
return PageResponse.of(marginMapper.selectList(param));
}
public MarginSummaryResp detailMargin(Long marginId) {
MarginSummaryResp resp = marginMapper.selectById(marginId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
/**
* 수지차 재계산 (recalc).
* settle_month 단위로 income_total(수입수수료 ) payout_total(settle_master net_amount ) 산출.
* 수입수수료 원장에는 설계사 귀속이 없어(보험사·계약 단위) income 전체 합을 쓰고,
* agentId 주어지면 payout 해당 설계사로 좁혀 집계한다. (미지정 GA 전체 수지차)
* 수지차 = income_total payout_total, 수지차율 = 수지차 / income_total (income_total > 0).
*/
@Transactional
public Long recalcMargin(String settleMonth, Long agentId) {
BigDecimal incomeTotal = marginMapper.sumIncomeByMonth(settleMonth);
if (incomeTotal == null) incomeTotal = BigDecimal.ZERO;
BigDecimal payoutTotal = marginMapper.sumPayoutByMonth(settleMonth, agentId);
if (payoutTotal == null) payoutTotal = BigDecimal.ZERO;
BigDecimal marginAmount = incomeTotal.subtract(payoutTotal).setScale(2, RoundingMode.HALF_UP);
BigDecimal marginRate = BigDecimal.ZERO;
if (incomeTotal.compareTo(BigDecimal.ZERO) > 0) {
marginRate = marginAmount.divide(incomeTotal, 4, RoundingMode.HALF_UP);
}
MarginSummaryVO existing = marginMapper.selectByMonthAndAgent(settleMonth, agentId);
if (existing != null) {
existing.setIncomeTotal(incomeTotal);
existing.setPayoutTotal(payoutTotal);
existing.setMarginAmount(marginAmount);
existing.setMarginRate(marginRate);
marginMapper.update(existing);
return existing.getMarginId();
} else {
MarginSummaryVO vo = new MarginSummaryVO();
vo.setSettleMonth(settleMonth);
vo.setAgentId(agentId);
vo.setIncomeTotal(incomeTotal);
vo.setPayoutTotal(payoutTotal);
vo.setMarginAmount(marginAmount);
vo.setMarginRate(marginRate);
marginMapper.insert(vo);
return vo.getMarginId();
}
}
}
@@ -0,0 +1,96 @@
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.JointContractMapper;
import com.ga.core.vo.commission.JointContractResp;
import com.ga.core.vo.commission.JointContractSaveReq;
import com.ga.core.vo.commission.JointContractSearchParam;
import com.ga.core.vo.commission.JointContractVO;
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 JointContractService {
private final JointContractMapper jointContractMapper;
public PageResponse<JointContractResp> list(JointContractSearchParam param) {
param.startPage();
return PageResponse.of(jointContractMapper.selectList(param));
}
public JointContractResp detail(Long jointId) {
JointContractResp resp = jointContractMapper.selectById(jointId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
/**
* 공동계약 생성.
* 기존 share_rate + 신규 share_rate > 1.0 이면 E411 예외.
*/
@Transactional
public Long create(JointContractSaveReq req) {
validateShareRate(req.getContractId(), null, req.getShareRate());
JointContractVO vo = new JointContractVO();
vo.setContractId(req.getContractId());
vo.setAgentId(req.getAgentId());
vo.setRole(req.getRole());
vo.setShareRate(req.getShareRate());
jointContractMapper.insert(vo);
return vo.getJointId();
}
/**
* 공동계약 수정.
* 기존 share_rate (자신 제외) + 수정 share_rate > 1.0 이면 E411 예외.
*/
@Transactional
public void update(Long jointId, JointContractSaveReq req) {
JointContractResp resp = jointContractMapper.selectById(jointId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
validateShareRate(req.getContractId(), jointId, req.getShareRate());
JointContractVO vo = new JointContractVO();
vo.setJointId(jointId);
vo.setContractId(req.getContractId());
vo.setAgentId(req.getAgentId());
vo.setRole(req.getRole());
vo.setShareRate(req.getShareRate());
jointContractMapper.update(vo);
}
@Transactional
public void delete(Long jointId) {
JointContractResp resp = jointContractMapper.selectById(jointId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
jointContractMapper.delete(jointId);
}
/**
* 계약별 share_rate 검증.
* excludeJointId: 수정 자신 레코드를 합산에서 제외하기 위한 파라미터 (null=create).
* sumShareByContract는 계약 전체 합을 반환하므로, 수정 기존 자신의 share_rate를 차감.
*/
private void validateShareRate(Long contractId, Long excludeJointId, BigDecimal newShareRate) {
BigDecimal existingSum = jointContractMapper.sumShareByContract(contractId);
if (existingSum == null) existingSum = BigDecimal.ZERO;
if (excludeJointId != null) {
JointContractResp existing = jointContractMapper.selectById(excludeJointId);
if (existing != null) {
existingSum = existingSum.subtract(existing.getShareRate());
}
}
if (existingSum.add(newShareRate).compareTo(BigDecimal.ONE) > 0) {
throw new BizException(ErrorCode.INVALID_PARAMETER, "계약별 지분율 합이 1을 초과합니다.");
}
}
}
@@ -0,0 +1,103 @@
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.LifeSupportLedgerMapper;
import com.ga.core.mapper.commission.LifeSupportRuleMapper;
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;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class LifeSupportService {
private final LifeSupportRuleMapper ruleMapper;
private final LifeSupportLedgerMapper ledgerMapper;
// Rule
public PageResponse<LifeSupportRuleResp> listRules(LifeSupportRuleSearchParam param) {
param.startPage();
return PageResponse.of(ruleMapper.selectList(param));
}
public LifeSupportRuleResp detailRule(Long ruleId) {
LifeSupportRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createRule(LifeSupportRuleSaveReq req) {
LifeSupportRuleVO vo = new LifeSupportRuleVO();
vo.setAgentGrade(req.getAgentGrade());
vo.setSupportRate(req.getSupportRate());
vo.setEffectiveFrom(req.getEffectiveFrom());
vo.setEffectiveTo(req.getEffectiveTo());
ruleMapper.insert(vo);
return vo.getRuleId();
}
@Transactional
public void updateRule(Long ruleId, LifeSupportRuleSaveReq req) {
LifeSupportRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
LifeSupportRuleVO vo = new LifeSupportRuleVO();
vo.setRuleId(ruleId);
vo.setAgentGrade(req.getAgentGrade());
vo.setSupportRate(req.getSupportRate());
vo.setEffectiveFrom(req.getEffectiveFrom());
vo.setEffectiveTo(req.getEffectiveTo());
ruleMapper.update(vo);
}
@Transactional
public void deleteRule(Long ruleId) {
LifeSupportRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
ruleMapper.delete(ruleId);
}
// Ledger
public PageResponse<LifeSupportLedgerResp> listLedgers(LifeSupportLedgerSearchParam param) {
param.startPage();
return PageResponse.of(ledgerMapper.selectList(param));
}
public LifeSupportLedgerResp detailLedger(Long ledgerId) {
LifeSupportLedgerResp resp = ledgerMapper.selectById(ledgerId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createLedger(LifeSupportLedgerSaveReq req) {
// 지원수수료 = base × support_rate
BigDecimal supportAmount = req.getBaseAmount()
.multiply(req.getSupportRate())
.setScale(2, RoundingMode.HALF_UP);
BigDecimal clawback = req.getClawbackAmount() != null ? req.getClawbackAmount() : BigDecimal.ZERO;
// 실지급액 = support - clawback
BigDecimal netAmount = supportAmount.subtract(clawback).setScale(2, RoundingMode.HALF_UP);
LifeSupportLedgerVO vo = new LifeSupportLedgerVO();
vo.setAgentId(req.getAgentId());
vo.setSettleMonth(req.getSettleMonth());
vo.setBaseAmount(req.getBaseAmount());
vo.setSupportRate(req.getSupportRate());
vo.setSupportAmount(supportAmount);
vo.setClawbackAmount(clawback);
vo.setNetAmount(netAmount);
vo.setStatus("CALCULATED");
ledgerMapper.insert(vo);
return vo.getLedgerId();
}
}
@@ -0,0 +1,105 @@
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.ProductConversionRateMapper;
import com.ga.core.mapper.commission.RecognizedPerformanceLedgerMapper;
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;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class RecognizedPerformanceService {
private final ProductConversionRateMapper conversionRateMapper;
private final RecognizedPerformanceLedgerMapper ledgerMapper;
// Product Conversion Rate (rule)
public PageResponse<ProductConversionRateResp> listRates(ProductConversionRateSearchParam param) {
param.startPage();
return PageResponse.of(conversionRateMapper.selectList(param));
}
public ProductConversionRateResp detailRate(Long rateId) {
ProductConversionRateResp resp = conversionRateMapper.selectById(rateId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createRate(ProductConversionRateSaveReq req) {
ProductConversionRateVO vo = new ProductConversionRateVO();
vo.setProductId(req.getProductId());
vo.setConversionRate(req.getConversionRate());
vo.setEffectiveFrom(req.getEffectiveFrom());
vo.setEffectiveTo(req.getEffectiveTo());
conversionRateMapper.insert(vo);
return vo.getRateId();
}
@Transactional
public void updateRate(Long rateId, ProductConversionRateSaveReq req) {
ProductConversionRateResp resp = conversionRateMapper.selectById(rateId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
ProductConversionRateVO vo = new ProductConversionRateVO();
vo.setRateId(rateId);
vo.setProductId(req.getProductId());
vo.setConversionRate(req.getConversionRate());
vo.setEffectiveFrom(req.getEffectiveFrom());
vo.setEffectiveTo(req.getEffectiveTo());
conversionRateMapper.update(vo);
}
@Transactional
public void deleteRate(Long rateId) {
ProductConversionRateResp resp = conversionRateMapper.selectById(rateId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
conversionRateMapper.delete(rateId);
}
// Ledger
public PageResponse<RecognizedPerformanceLedgerResp> listLedgers(RecognizedPerformanceLedgerSearchParam param) {
param.startPage();
return PageResponse.of(ledgerMapper.selectList(param));
}
public RecognizedPerformanceLedgerResp detailLedger(Long ledgerId) {
RecognizedPerformanceLedgerResp resp = ledgerMapper.selectById(ledgerId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createLedger(RecognizedPerformanceLedgerSaveReq req) {
// 환산보험료 = premium × conversion_rate
BigDecimal convertedPremium = req.getPremiumAmount()
.multiply(req.getConversionRate())
.setScale(2, RoundingMode.HALF_UP);
// 인정실적 = converted_premium × recognition_rate
BigDecimal recognizedAmount = convertedPremium
.multiply(req.getRecognitionRate())
.setScale(2, RoundingMode.HALF_UP);
RecognizedPerformanceLedgerVO vo = new RecognizedPerformanceLedgerVO();
vo.setAgentId(req.getAgentId());
vo.setContractId(req.getContractId());
vo.setSettleMonth(req.getSettleMonth());
vo.setPremiumAmount(req.getPremiumAmount());
vo.setConversionRate(req.getConversionRate());
vo.setConvertedPremium(convertedPremium);
vo.setRecognitionRate(req.getRecognitionRate());
vo.setRecognizedAmount(recognizedAmount);
vo.setStatus("CALCULATED");
ledgerMapper.insert(vo);
return vo.getLedgerId();
}
}
@@ -0,0 +1,115 @@
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.ReferralCommissionLedgerMapper;
import com.ga.core.mapper.commission.ReferralCommissionRuleMapper;
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;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ReferralCommissionService {
private final ReferralCommissionRuleMapper ruleMapper;
private final ReferralCommissionLedgerMapper ledgerMapper;
// Rule
public PageResponse<ReferralCommissionRuleResp> listRules(ReferralCommissionRuleSearchParam param) {
param.startPage();
return PageResponse.of(ruleMapper.selectList(param));
}
public ReferralCommissionRuleResp detailRule(Long ruleId) {
ReferralCommissionRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createRule(ReferralCommissionRuleSaveReq req) {
ReferralCommissionRuleVO vo = new ReferralCommissionRuleVO();
vo.setReferralType(req.getReferralType());
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, ReferralCommissionRuleSaveReq req) {
ReferralCommissionRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
ReferralCommissionRuleVO vo = new ReferralCommissionRuleVO();
vo.setRuleId(ruleId);
vo.setReferralType(req.getReferralType());
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) {
ReferralCommissionRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
ruleMapper.delete(ruleId);
}
// Ledger
public PageResponse<ReferralCommissionLedgerResp> listLedgers(ReferralCommissionLedgerSearchParam param) {
param.startPage();
return PageResponse.of(ledgerMapper.selectList(param));
}
public ReferralCommissionLedgerResp detailLedger(Long ledgerId) {
ReferralCommissionLedgerResp resp = ledgerMapper.selectById(ledgerId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createLedger(ReferralCommissionLedgerSaveReq req) {
ReferralCommissionLedgerVO vo = new ReferralCommissionLedgerVO();
vo.setContractId(req.getContractId());
vo.setReferrerAgentId(req.getReferrerAgentId());
vo.setSettleMonth(req.getSettleMonth());
vo.setReferralType(req.getReferralType());
vo.setBaseAmount(req.getBaseAmount());
vo.setCommissionRate(req.getCommissionRate());
// 소개·이관수수료 = base × rate (RATE) 또는 fixed_amount (FIXED)
BigDecimal commissionAmount;
if (req.getCommissionRate() != null) {
commissionAmount = req.getBaseAmount()
.multiply(req.getCommissionRate())
.setScale(2, RoundingMode.HALF_UP);
} else {
// FIXED: 룰에서 fixed_amount 조회
ReferralCommissionRuleVO rule = ruleMapper.selectActiveRule(
req.getReferralType(), java.time.LocalDate.now());
if (rule == null || rule.getFixedAmount() == null) {
throw new BizException(ErrorCode.NOT_FOUND);
}
commissionAmount = rule.getFixedAmount().setScale(2, RoundingMode.HALF_UP);
}
vo.setCommissionAmount(commissionAmount);
vo.setStatus("CALCULATED");
ledgerMapper.insert(vo);
return vo.getLedgerId();
}
}
@@ -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.model.PageResponse;
import com.ga.core.mapper.commission.AgentReserveFundMapper;
import com.ga.core.vo.commission.AgentReserveFundResp;
import com.ga.core.vo.commission.AgentReserveFundSaveReq;
import com.ga.core.vo.commission.AgentReserveFundSearchParam;
import com.ga.core.vo.commission.AgentReserveFundVO;
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)
public class ReserveFundService {
private final AgentReserveFundMapper reserveFundMapper;
public PageResponse<AgentReserveFundResp> list(AgentReserveFundSearchParam param) {
param.startPage();
return PageResponse.of(reserveFundMapper.selectList(param));
}
public AgentReserveFundResp detail(Long reserveId) {
AgentReserveFundResp resp = reserveFundMapper.selectById(reserveId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
/**
* 적립금 생성(upsert).
* balance = (직전월 balance or 0) + reserveIn reserveOut. setScale(2, HALF_UP).
* 해당 레코드가 이미 존재하면 update, 없으면 insert.
*/
@Transactional
public Long create(AgentReserveFundSaveReq req) {
AgentReserveFundVO prevMonth = reserveFundMapper.selectLatestBefore(req.getAgentId(), req.getSettleMonth());
BigDecimal prevBalance = (prevMonth != null && prevMonth.getBalance() != null)
? prevMonth.getBalance()
: BigDecimal.ZERO;
BigDecimal balance = prevBalance
.add(req.getReserveIn())
.subtract(req.getReserveOut())
.setScale(2, RoundingMode.HALF_UP);
AgentReserveFundVO existing = reserveFundMapper.selectByAgentAndMonth(req.getAgentId(), req.getSettleMonth());
if (existing != null) {
existing.setReserveIn(req.getReserveIn());
existing.setReserveOut(req.getReserveOut());
existing.setBalance(balance);
reserveFundMapper.update(existing);
return existing.getReserveId();
}
AgentReserveFundVO vo = new AgentReserveFundVO();
vo.setAgentId(req.getAgentId());
vo.setSettleMonth(req.getSettleMonth());
vo.setReserveIn(req.getReserveIn());
vo.setReserveOut(req.getReserveOut());
vo.setBalance(balance);
reserveFundMapper.insert(vo);
return vo.getReserveId();
}
}
@@ -0,0 +1,103 @@
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.SettlingSupportLedgerMapper;
import com.ga.core.mapper.commission.SettlingSupportRuleMapper;
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;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class SettlingSupportService {
private final SettlingSupportRuleMapper ruleMapper;
private final SettlingSupportLedgerMapper ledgerMapper;
// Rule
public PageResponse<SettlingSupportRuleResp> listRules(SettlingSupportRuleSearchParam param) {
param.startPage();
return PageResponse.of(ruleMapper.selectList(param));
}
public SettlingSupportRuleResp detailRule(Long ruleId) {
SettlingSupportRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createRule(SettlingSupportRuleSaveReq req) {
SettlingSupportRuleVO vo = new SettlingSupportRuleVO();
vo.setAgentGrade(req.getAgentGrade());
vo.setSupportAmount(req.getSupportAmount());
vo.setDurationMonths(req.getDurationMonths());
vo.setClawbackMonths(req.getClawbackMonths());
vo.setEffectiveFrom(req.getEffectiveFrom());
vo.setEffectiveTo(req.getEffectiveTo());
ruleMapper.insert(vo);
return vo.getRuleId();
}
@Transactional
public void updateRule(Long ruleId, SettlingSupportRuleSaveReq req) {
SettlingSupportRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
SettlingSupportRuleVO vo = new SettlingSupportRuleVO();
vo.setRuleId(ruleId);
vo.setAgentGrade(req.getAgentGrade());
vo.setSupportAmount(req.getSupportAmount());
vo.setDurationMonths(req.getDurationMonths());
vo.setClawbackMonths(req.getClawbackMonths());
vo.setEffectiveFrom(req.getEffectiveFrom());
vo.setEffectiveTo(req.getEffectiveTo());
ruleMapper.update(vo);
}
@Transactional
public void deleteRule(Long ruleId) {
SettlingSupportRuleResp resp = ruleMapper.selectById(ruleId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
ruleMapper.delete(ruleId);
}
// Ledger
public PageResponse<SettlingSupportLedgerResp> listLedgers(SettlingSupportLedgerSearchParam param) {
param.startPage();
return PageResponse.of(ledgerMapper.selectList(param));
}
public SettlingSupportLedgerResp detailLedger(Long ledgerId) {
SettlingSupportLedgerResp resp = ledgerMapper.selectById(ledgerId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long createLedger(SettlingSupportLedgerSaveReq req) {
BigDecimal clawback = req.getClawbackAmount() != null ? req.getClawbackAmount() : BigDecimal.ZERO;
// 실지급액 = support_amount - clawback_amount
BigDecimal netAmount = req.getSupportAmount()
.subtract(clawback)
.setScale(2, RoundingMode.HALF_UP);
SettlingSupportLedgerVO vo = new SettlingSupportLedgerVO();
vo.setAgentId(req.getAgentId());
vo.setSettleMonth(req.getSettleMonth());
vo.setSupportAmount(req.getSupportAmount());
vo.setClawbackAmount(clawback);
vo.setNetAmount(netAmount);
vo.setStatus("CALCULATED");
ledgerMapper.insert(vo);
return vo.getLedgerId();
}
}
@@ -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.common.util.SecurityUtil;
import com.ga.core.mapper.commission.StepCloseMapper;
import com.ga.core.vo.commission.StepCloseResp;
import com.ga.core.vo.commission.StepCloseSaveReq;
import com.ga.core.vo.commission.StepCloseSearchParam;
import com.ga.core.vo.commission.StepCloseVO;
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 StepCloseService {
private final StepCloseMapper stepCloseMapper;
public PageResponse<StepCloseResp> list(StepCloseSearchParam param) {
param.startPage();
return PageResponse.of(stepCloseMapper.selectList(param));
}
public List<StepCloseResp> listByMonth(String settleMonth) {
return stepCloseMapper.selectBySettleMonth(settleMonth);
}
public StepCloseResp detail(Long stepCloseId) {
StepCloseResp resp = stepCloseMapper.selectById(stepCloseId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
return resp;
}
@Transactional
public Long create(StepCloseSaveReq req) {
StepCloseVO vo = new StepCloseVO();
vo.setSettleMonth(req.getSettleMonth());
vo.setStepCode(req.getStepCode());
vo.setStepSeq(req.getStepSeq());
vo.setStatus("OPEN");
stepCloseMapper.insert(vo);
return vo.getStepCloseId();
}
@Transactional
public void close(Long stepCloseId) {
StepCloseResp resp = stepCloseMapper.selectById(stepCloseId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
if ("CLOSED".equals(resp.getStatus())) {
throw new BizException(ErrorCode.INVALID_STATUS, "이미 마감된 단계입니다.");
}
stepCloseMapper.updateStatus(stepCloseId, "CLOSED", SecurityUtil.getCurrentUserId());
}
@Transactional
public void reopen(Long stepCloseId) {
StepCloseResp resp = stepCloseMapper.selectById(stepCloseId);
if (resp == null) throw new BizException(ErrorCode.NOT_FOUND);
if ("OPEN".equals(resp.getStatus())) {
throw new BizException(ErrorCode.INVALID_STATUS, "이미 열려 있는 단계입니다.");
}
stepCloseMapper.updateStatus(stepCloseId, "OPEN", SecurityUtil.getCurrentUserId());
}
}
@@ -0,0 +1,77 @@
package com.ga.api.service.commission;
import com.ga.core.mapper.commission.AwardLedgerMapper;
import com.ga.core.mapper.commission.AwardRuleMapper;
import com.ga.core.vo.commission.AwardLedgerSaveReq;
import com.ga.core.vo.commission.AwardLedgerVO;
import com.ga.core.vo.commission.AwardRuleVO;
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.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class AwardServiceTest {
@Mock AwardRuleMapper ruleMapper;
@Mock AwardLedgerMapper ledgerMapper;
@InjectMocks AwardService service;
@Test
@DisplayName("createLedger RATE: award_amount = base × award_rate (1,000,000 × 0.05 = 50,000)")
void createLedger_rate() {
AwardRuleVO rule = new AwardRuleVO();
rule.setCalcType("RATE");
rule.setAwardRate(new BigDecimal("0.05"));
when(ruleMapper.selectActiveRule(eq("PERFORMANCE"), any())).thenReturn(rule);
AwardLedgerSaveReq req = new AwardLedgerSaveReq();
req.setAgentId(1L);
req.setSettleMonth("202506");
req.setAwardType("PERFORMANCE");
req.setBaseAmount(new BigDecimal("1000000"));
ArgumentCaptor<AwardLedgerVO> captor = ArgumentCaptor.forClass(AwardLedgerVO.class);
when(ledgerMapper.insert(captor.capture())).thenReturn(1);
service.createLedger(req);
AwardLedgerVO saved = captor.getValue();
assertThat(saved.getAwardAmount()).isEqualByComparingTo("50000.00");
assertThat(saved.getStatus()).isEqualTo("CALCULATED");
}
@Test
@DisplayName("createLedger FIXED: award_amount = award_fixed (200,000)")
void createLedger_fixed() {
AwardRuleVO rule = new AwardRuleVO();
rule.setCalcType("FIXED");
rule.setAwardFixed(new BigDecimal("200000"));
when(ruleMapper.selectActiveRule(eq("NEW_RECRUIT"), any())).thenReturn(rule);
AwardLedgerSaveReq req = new AwardLedgerSaveReq();
req.setAgentId(2L);
req.setSettleMonth("202506");
req.setAwardType("NEW_RECRUIT");
req.setBaseAmount(new BigDecimal("999999")); // FIXED이므로 base는 무시됨
ArgumentCaptor<AwardLedgerVO> captor = ArgumentCaptor.forClass(AwardLedgerVO.class);
when(ledgerMapper.insert(captor.capture())).thenReturn(1);
service.createLedger(req);
AwardLedgerVO saved = captor.getValue();
assertThat(saved.getAwardAmount()).isEqualByComparingTo("200000.00");
}
}
@@ -0,0 +1,48 @@
package com.ga.api.service.commission;
import com.ga.core.mapper.commission.BranchMgrAllowanceLedgerMapper;
import com.ga.core.mapper.commission.BranchMgrAllowanceRuleMapper;
import com.ga.core.vo.commission.BranchMgrAllowanceLedgerSaveReq;
import com.ga.core.vo.commission.BranchMgrAllowanceLedgerVO;
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.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class BranchAllowanceServiceTest {
@Mock BranchMgrAllowanceRuleMapper ruleMapper;
@Mock BranchMgrAllowanceLedgerMapper ledgerMapper;
@InjectMocks BranchAllowanceService service;
@Test
@DisplayName("createLedger: allowance = base × allowance_rate (2,000,000 × 0.05 = 100,000)")
void createLedger_allowanceAmount() {
BranchMgrAllowanceLedgerSaveReq req = new BranchMgrAllowanceLedgerSaveReq();
req.setManagerAgentId(1L);
req.setOrgId(10L);
req.setSettleMonth("202506");
req.setBaseAmount(new BigDecimal("2000000"));
req.setAllowanceRate(new BigDecimal("0.05"));
req.setClawbackAmount(null);
ArgumentCaptor<BranchMgrAllowanceLedgerVO> captor = ArgumentCaptor.forClass(BranchMgrAllowanceLedgerVO.class);
when(ledgerMapper.insert(captor.capture())).thenReturn(1);
service.createLedger(req);
BranchMgrAllowanceLedgerVO saved = captor.getValue();
assertThat(saved.getAllowanceAmount()).isEqualByComparingTo("100000.00");
assertThat(saved.getStatus()).isEqualTo("CALCULATED");
}
}
@@ -0,0 +1,69 @@
package com.ga.api.service.commission;
import com.ga.common.util.SecurityUtil;
import com.ga.core.mapper.commission.CommissionSimulationMapper;
import com.ga.core.vo.commission.CommissionSimulationResp;
import com.ga.core.vo.commission.CommissionSimulationSaveReq;
import com.ga.core.vo.commission.CommissionSimulationVO;
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.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import java.math.BigDecimal;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class CommissionSimulationServiceTest {
@Mock CommissionSimulationMapper simulationMapper;
@InjectMocks CommissionSimulationService service;
/**
* 1,000,000 × 0.6 = 600,000 (company_payment)
* 600,000 × 0.75 = 450,000 (agent_commission)
* 450,000 × 0.033 = 14,850 (withholding_tax)
* 450,000 - 14,850 = 435,150 (net_amount)
* 450,000 × 0.5 = 225,000 (expected_chargeback 자동 계산)
*/
@Test
@DisplayName("simulate: 수수료 공식 전체 검증 (1,000,000/0.6/0.75)")
void simulate_fullFormula() {
CommissionSimulationSaveReq req = new CommissionSimulationSaveReq();
req.setSimName("테스트시뮬레이션");
req.setPremiumAmount(new BigDecimal("1000000"));
req.setCompanyRate(new BigDecimal("0.6"));
req.setPayoutRate(new BigDecimal("0.75"));
req.setCommissionYear(1);
req.setExpectedChargeback(null); // 자동 계산
ArgumentCaptor<CommissionSimulationVO> captor = ArgumentCaptor.forClass(CommissionSimulationVO.class);
when(simulationMapper.insert(captor.capture())).thenReturn(1);
CommissionSimulationResp mockResp = new CommissionSimulationResp();
mockResp.setSimId(1L);
when(simulationMapper.selectById(any())).thenReturn(mockResp);
try (MockedStatic<SecurityUtil> staticMock = mockStatic(SecurityUtil.class)) {
staticMock.when(SecurityUtil::getCurrentUserId).thenReturn(null);
service.simulate(req);
}
CommissionSimulationVO saved = captor.getValue();
assertThat(saved.getCompanyPayment()).isEqualByComparingTo("600000.00");
assertThat(saved.getAgentCommission()).isEqualByComparingTo("450000.00");
assertThat(saved.getWithholdingTax()).isEqualByComparingTo("14850.00");
assertThat(saved.getNetAmount()).isEqualByComparingTo("435150.00");
assertThat(saved.getExpectedChargeback()).isEqualByComparingTo("225000.00");
}
}
@@ -0,0 +1,72 @@
package com.ga.api.service.commission;
import com.ga.core.mapper.commission.AgentGradeEvaluationMapper;
import com.ga.core.mapper.commission.GradeEvalRuleMapper;
import com.ga.core.vo.commission.AgentGradeEvaluationSaveReq;
import com.ga.core.vo.commission.AgentGradeEvaluationVO;
import com.ga.core.vo.commission.GradeEvalRuleVO;
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.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class GradeEvaluationServiceTest {
@Mock GradeEvalRuleMapper ruleMapper;
@Mock AgentGradeEvaluationMapper evalMapper;
@InjectMocks GradeEvaluationService service;
@Test
@DisplayName("createEval: 매칭 룰이 있으면 evaluated_grade_id = rule.gradeId")
void createEval_ruleMatched() {
GradeEvalRuleVO rule = new GradeEvalRuleVO();
rule.setGradeId(5L);
when(ruleMapper.selectActiveRuleByScore(any(), any())).thenReturn(rule);
AgentGradeEvaluationSaveReq req = new AgentGradeEvaluationSaveReq();
req.setAgentId(1L);
req.setEvalPeriod("202506");
req.setPerformanceScore(new BigDecimal("850"));
req.setPrevGradeId(3L);
ArgumentCaptor<AgentGradeEvaluationVO> captor = ArgumentCaptor.forClass(AgentGradeEvaluationVO.class);
when(evalMapper.insert(captor.capture())).thenReturn(1);
service.createEval(req);
AgentGradeEvaluationVO saved = captor.getValue();
assertThat(saved.getEvaluatedGradeId()).isEqualTo(5L);
assertThat(saved.getStatus()).isEqualTo("EVALUATED");
}
@Test
@DisplayName("createEval: 매칭 룰 없으면 evaluated_grade_id = prevGradeId 유지")
void createEval_noRuleMatch_usePrevGrade() {
when(ruleMapper.selectActiveRuleByScore(any(), any())).thenReturn(null);
AgentGradeEvaluationSaveReq req = new AgentGradeEvaluationSaveReq();
req.setAgentId(1L);
req.setEvalPeriod("202506");
req.setPerformanceScore(new BigDecimal("100"));
req.setPrevGradeId(2L);
ArgumentCaptor<AgentGradeEvaluationVO> captor = ArgumentCaptor.forClass(AgentGradeEvaluationVO.class);
when(evalMapper.insert(captor.capture())).thenReturn(1);
service.createEval(req);
AgentGradeEvaluationVO saved = captor.getValue();
assertThat(saved.getEvaluatedGradeId()).isEqualTo(2L);
}
}
@@ -0,0 +1,47 @@
package com.ga.api.service.commission;
import com.ga.core.mapper.commission.HqSettleItemMapper;
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 static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class HqSettleServiceTest {
@Mock HqSettleItemMapper itemMapper;
@InjectMocks HqSettleService service;
@Test
@DisplayName("summary: profit = income - expense (3,000,000 - 1,200,000 = 1,800,000)")
void summary_profitCalc() {
when(itemMapper.sumByMonthAndType("202506", "INCOME")).thenReturn(new BigDecimal("3000000"));
when(itemMapper.sumByMonthAndType("202506", "EXPENSE")).thenReturn(new BigDecimal("1200000"));
HqSettleService.HqSummaryResp result = service.summary("202506");
assertThat(result.getIncome()).isEqualByComparingTo("3000000");
assertThat(result.getExpense()).isEqualByComparingTo("1200000");
assertThat(result.getProfit()).isEqualByComparingTo("1800000");
assertThat(result.getSettleMonth()).isEqualTo("202506");
}
@Test
@DisplayName("summary: income/expense null이면 0으로 처리 → profit = 0")
void summary_nullHandling() {
when(itemMapper.sumByMonthAndType("202506", "INCOME")).thenReturn(null);
when(itemMapper.sumByMonthAndType("202506", "EXPENSE")).thenReturn(null);
HqSettleService.HqSummaryResp result = service.summary("202506");
assertThat(result.getProfit()).isEqualByComparingTo("0");
}
}
@@ -0,0 +1,67 @@
package com.ga.api.service.commission;
import com.ga.core.mapper.commission.IncomeCommissionLedgerMapper;
import com.ga.core.mapper.commission.MarginSummaryMapper;
import com.ga.core.vo.commission.IncomeCommissionLedgerSaveReq;
import com.ga.core.vo.commission.IncomeCommissionLedgerVO;
import com.ga.core.vo.commission.MarginSummaryVO;
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.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class IncomeCommissionServiceTest {
@Mock IncomeCommissionLedgerMapper ledgerMapper;
@Mock MarginSummaryMapper marginMapper;
@InjectMocks IncomeCommissionService service;
@Test
@DisplayName("createLedger: income_amount = premium × company_rate (1,000,000 × 0.6 = 600,000)")
void createLedger_incomeAmount() {
IncomeCommissionLedgerSaveReq req = new IncomeCommissionLedgerSaveReq();
req.setContractId(1L);
req.setCarrierId(10L);
req.setSettleMonth("202506");
req.setPremiumAmount(new BigDecimal("1000000"));
req.setCompanyRate(new BigDecimal("0.6"));
req.setCommissionType("RECRUIT");
ArgumentCaptor<IncomeCommissionLedgerVO> captor = ArgumentCaptor.forClass(IncomeCommissionLedgerVO.class);
when(ledgerMapper.insert(captor.capture())).thenReturn(1);
service.createLedger(req);
IncomeCommissionLedgerVO saved = captor.getValue();
assertThat(saved.getIncomeAmount()).isEqualByComparingTo("600000.00");
assertThat(saved.getStatus()).isEqualTo("CALCULATED");
}
@Test
@DisplayName("recalcMargin: margin = income - payout, marginRate = margin / income")
void recalcMargin_calculation() {
when(marginMapper.sumIncomeByMonth("202506")).thenReturn(new BigDecimal("1000000"));
when(marginMapper.sumPayoutByMonth("202506", null)).thenReturn(new BigDecimal("600000"));
when(marginMapper.selectByMonthAndAgent("202506", null)).thenReturn(null);
ArgumentCaptor<MarginSummaryVO> captor = ArgumentCaptor.forClass(MarginSummaryVO.class);
when(marginMapper.insert(captor.capture())).thenReturn(1);
service.recalcMargin("202506", null);
MarginSummaryVO saved = captor.getValue();
assertThat(saved.getMarginAmount()).isEqualByComparingTo("400000.00");
assertThat(saved.getMarginRate()).isEqualByComparingTo("0.4000");
}
}
@@ -0,0 +1,58 @@
package com.ga.api.service.commission;
import com.ga.common.exception.BizException;
import com.ga.core.mapper.commission.JointContractMapper;
import com.ga.core.vo.commission.JointContractSaveReq;
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 static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class JointContractServiceTest {
@Mock JointContractMapper jointContractMapper;
@InjectMocks JointContractService service;
@Test
@DisplayName("create: 기존 지분율 합 + 신규 shareRate > 1이면 BizException 발생")
void create_shareRateExceedsOne_throwsBizException() {
// 기존 0.7, 신규 0.4 합계 1.1 > 1
when(jointContractMapper.sumShareByContract(eq(100L))).thenReturn(new BigDecimal("0.7"));
JointContractSaveReq req = new JointContractSaveReq();
req.setContractId(100L);
req.setAgentId(5L);
req.setRole("MAIN");
req.setShareRate(new BigDecimal("0.4"));
assertThatThrownBy(() -> service.create(req))
.isInstanceOf(BizException.class);
}
@Test
@DisplayName("create: 기존 합 + 신규 shareRate <= 1이면 정상 생성")
void create_shareRateValid_noException() {
// 기존 0.6, 신규 0.4 합계 1.0 = 1 (경계값 허용)
when(jointContractMapper.sumShareByContract(eq(100L))).thenReturn(new BigDecimal("0.6"));
when(jointContractMapper.insert(org.mockito.ArgumentMatchers.any())).thenReturn(1);
JointContractSaveReq req = new JointContractSaveReq();
req.setContractId(100L);
req.setAgentId(5L);
req.setRole("SUB");
req.setShareRate(new BigDecimal("0.4"));
// 예외 없이 통과해야
service.create(req);
}
}
@@ -0,0 +1,68 @@
package com.ga.api.service.commission;
import com.ga.core.mapper.commission.LifeSupportLedgerMapper;
import com.ga.core.mapper.commission.LifeSupportRuleMapper;
import com.ga.core.vo.commission.LifeSupportLedgerSaveReq;
import com.ga.core.vo.commission.LifeSupportLedgerVO;
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.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class LifeSupportServiceTest {
@Mock LifeSupportRuleMapper ruleMapper;
@Mock LifeSupportLedgerMapper ledgerMapper;
@InjectMocks LifeSupportService service;
@Test
@DisplayName("createLedger: support = base×rate, net = support - clawback (1,000,000×0.3=300,000, 300,000-20,000=280,000)")
void createLedger_calculation() {
LifeSupportLedgerSaveReq req = new LifeSupportLedgerSaveReq();
req.setAgentId(1L);
req.setSettleMonth("202506");
req.setBaseAmount(new BigDecimal("1000000"));
req.setSupportRate(new BigDecimal("0.3"));
req.setClawbackAmount(new BigDecimal("20000"));
ArgumentCaptor<LifeSupportLedgerVO> captor = ArgumentCaptor.forClass(LifeSupportLedgerVO.class);
when(ledgerMapper.insert(captor.capture())).thenReturn(1);
service.createLedger(req);
LifeSupportLedgerVO saved = captor.getValue();
assertThat(saved.getSupportAmount()).isEqualByComparingTo("300000.00");
assertThat(saved.getNetAmount()).isEqualByComparingTo("280000.00");
assertThat(saved.getStatus()).isEqualTo("CALCULATED");
}
@Test
@DisplayName("createLedger: clawback null이면 0으로 처리 → net = support")
void createLedger_noClawback() {
LifeSupportLedgerSaveReq req = new LifeSupportLedgerSaveReq();
req.setAgentId(1L);
req.setSettleMonth("202506");
req.setBaseAmount(new BigDecimal("500000"));
req.setSupportRate(new BigDecimal("0.2"));
req.setClawbackAmount(null);
ArgumentCaptor<LifeSupportLedgerVO> captor = ArgumentCaptor.forClass(LifeSupportLedgerVO.class);
when(ledgerMapper.insert(captor.capture())).thenReturn(1);
service.createLedger(req);
LifeSupportLedgerVO saved = captor.getValue();
assertThat(saved.getSupportAmount()).isEqualByComparingTo("100000.00");
assertThat(saved.getNetAmount()).isEqualByComparingTo("100000.00");
}
}
@@ -0,0 +1,49 @@
package com.ga.api.service.commission;
import com.ga.core.mapper.commission.ProductConversionRateMapper;
import com.ga.core.mapper.commission.RecognizedPerformanceLedgerMapper;
import com.ga.core.vo.commission.RecognizedPerformanceLedgerSaveReq;
import com.ga.core.vo.commission.RecognizedPerformanceLedgerVO;
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.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class RecognizedPerformanceServiceTest {
@Mock ProductConversionRateMapper conversionRateMapper;
@Mock RecognizedPerformanceLedgerMapper ledgerMapper;
@InjectMocks RecognizedPerformanceService service;
@Test
@DisplayName("createLedger: converted=premium×conversion_rate, recognized=converted×recognition_rate (1,000,000×0.5×0.8=400,000)")
void createLedger_twoStepCalc() {
RecognizedPerformanceLedgerSaveReq req = new RecognizedPerformanceLedgerSaveReq();
req.setAgentId(1L);
req.setContractId(2L);
req.setSettleMonth("202506");
req.setPremiumAmount(new BigDecimal("1000000"));
req.setConversionRate(new BigDecimal("0.5"));
req.setRecognitionRate(new BigDecimal("0.8"));
ArgumentCaptor<RecognizedPerformanceLedgerVO> captor = ArgumentCaptor.forClass(RecognizedPerformanceLedgerVO.class);
when(ledgerMapper.insert(captor.capture())).thenReturn(1);
service.createLedger(req);
RecognizedPerformanceLedgerVO saved = captor.getValue();
assertThat(saved.getConvertedPremium()).isEqualByComparingTo("500000.00");
assertThat(saved.getRecognizedAmount()).isEqualByComparingTo("400000.00");
assertThat(saved.getStatus()).isEqualTo("CALCULATED");
}
}
@@ -0,0 +1,75 @@
package com.ga.api.service.commission;
import com.ga.core.mapper.commission.ReferralCommissionLedgerMapper;
import com.ga.core.mapper.commission.ReferralCommissionRuleMapper;
import com.ga.core.vo.commission.ReferralCommissionLedgerSaveReq;
import com.ga.core.vo.commission.ReferralCommissionLedgerVO;
import com.ga.core.vo.commission.ReferralCommissionRuleVO;
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.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ReferralCommissionServiceTest {
@Mock ReferralCommissionRuleMapper ruleMapper;
@Mock ReferralCommissionLedgerMapper ledgerMapper;
@InjectMocks ReferralCommissionService service;
@Test
@DisplayName("createLedger RATE: commission_amount = base × rate (500,000 × 0.1 = 50,000)")
void createLedger_rate() {
ReferralCommissionLedgerSaveReq req = new ReferralCommissionLedgerSaveReq();
req.setContractId(1L);
req.setReferrerAgentId(2L);
req.setSettleMonth("202506");
req.setReferralType("REFERRAL");
req.setBaseAmount(new BigDecimal("500000"));
req.setCommissionRate(new BigDecimal("0.1"));
ArgumentCaptor<ReferralCommissionLedgerVO> captor = ArgumentCaptor.forClass(ReferralCommissionLedgerVO.class);
when(ledgerMapper.insert(captor.capture())).thenReturn(1);
service.createLedger(req);
ReferralCommissionLedgerVO saved = captor.getValue();
assertThat(saved.getCommissionAmount()).isEqualByComparingTo("50000.00");
assertThat(saved.getStatus()).isEqualTo("CALCULATED");
}
@Test
@DisplayName("createLedger FIXED: commission_amount = rule.fixedAmount (30,000)")
void createLedger_fixed() {
ReferralCommissionLedgerSaveReq req = new ReferralCommissionLedgerSaveReq();
req.setContractId(1L);
req.setReferrerAgentId(2L);
req.setSettleMonth("202506");
req.setReferralType("TRANSFER");
req.setBaseAmount(new BigDecimal("500000"));
req.setCommissionRate(null); // FIXED 분기
ReferralCommissionRuleVO rule = new ReferralCommissionRuleVO();
rule.setCalcType("FIXED");
rule.setFixedAmount(new BigDecimal("30000"));
when(ruleMapper.selectActiveRule(any(), any())).thenReturn(rule);
ArgumentCaptor<ReferralCommissionLedgerVO> captor = ArgumentCaptor.forClass(ReferralCommissionLedgerVO.class);
when(ledgerMapper.insert(captor.capture())).thenReturn(1);
service.createLedger(req);
ReferralCommissionLedgerVO saved = captor.getValue();
assertThat(saved.getCommissionAmount()).isEqualByComparingTo("30000.00");
}
}
@@ -0,0 +1,71 @@
package com.ga.api.service.commission;
import com.ga.core.mapper.commission.AgentReserveFundMapper;
import com.ga.core.vo.commission.AgentReserveFundSaveReq;
import com.ga.core.vo.commission.AgentReserveFundVO;
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.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ReserveFundServiceTest {
@Mock AgentReserveFundMapper reserveFundMapper;
@InjectMocks ReserveFundService service;
@Test
@DisplayName("create: balance = 직전월잔액 + reserveIn - reserveOut (100,000 + 50,000 - 20,000 = 130,000)")
void create_balanceCalc() {
AgentReserveFundVO prevMonth = new AgentReserveFundVO();
prevMonth.setBalance(new BigDecimal("100000"));
when(reserveFundMapper.selectLatestBefore(eq(1L), eq("202506"))).thenReturn(prevMonth);
when(reserveFundMapper.selectByAgentAndMonth(eq(1L), eq("202506"))).thenReturn(null);
AgentReserveFundSaveReq req = new AgentReserveFundSaveReq();
req.setAgentId(1L);
req.setSettleMonth("202506");
req.setReserveIn(new BigDecimal("50000"));
req.setReserveOut(new BigDecimal("20000"));
ArgumentCaptor<AgentReserveFundVO> captor = ArgumentCaptor.forClass(AgentReserveFundVO.class);
when(reserveFundMapper.insert(captor.capture())).thenReturn(1);
service.create(req);
AgentReserveFundVO saved = captor.getValue();
assertThat(saved.getBalance()).isEqualByComparingTo("130000.00");
}
@Test
@DisplayName("create: 직전월 잔액 없으면 0으로 처리 → balance = reserveIn - reserveOut")
void create_noPrevMonth() {
when(reserveFundMapper.selectLatestBefore(eq(1L), eq("202501"))).thenReturn(null);
when(reserveFundMapper.selectByAgentAndMonth(eq(1L), eq("202501"))).thenReturn(null);
AgentReserveFundSaveReq req = new AgentReserveFundSaveReq();
req.setAgentId(1L);
req.setSettleMonth("202501");
req.setReserveIn(new BigDecimal("80000"));
req.setReserveOut(new BigDecimal("30000"));
ArgumentCaptor<AgentReserveFundVO> captor = ArgumentCaptor.forClass(AgentReserveFundVO.class);
when(reserveFundMapper.insert(captor.capture())).thenReturn(1);
service.create(req);
AgentReserveFundVO saved = captor.getValue();
assertThat(saved.getBalance()).isEqualByComparingTo("50000.00");
}
}
@@ -0,0 +1,65 @@
package com.ga.api.service.commission;
import com.ga.core.mapper.commission.SettlingSupportLedgerMapper;
import com.ga.core.mapper.commission.SettlingSupportRuleMapper;
import com.ga.core.vo.commission.SettlingSupportLedgerSaveReq;
import com.ga.core.vo.commission.SettlingSupportLedgerVO;
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.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class SettlingSupportServiceTest {
@Mock SettlingSupportRuleMapper ruleMapper;
@Mock SettlingSupportLedgerMapper ledgerMapper;
@InjectMocks SettlingSupportService service;
@Test
@DisplayName("createLedger: net_amount = support_amount - clawback_amount (500,000 - 50,000 = 450,000)")
void createLedger_netAmount() {
SettlingSupportLedgerSaveReq req = new SettlingSupportLedgerSaveReq();
req.setAgentId(1L);
req.setSettleMonth("202506");
req.setSupportAmount(new BigDecimal("500000"));
req.setClawbackAmount(new BigDecimal("50000"));
ArgumentCaptor<SettlingSupportLedgerVO> captor = ArgumentCaptor.forClass(SettlingSupportLedgerVO.class);
when(ledgerMapper.insert(captor.capture())).thenReturn(1);
service.createLedger(req);
SettlingSupportLedgerVO saved = captor.getValue();
assertThat(saved.getNetAmount()).isEqualByComparingTo("450000.00");
assertThat(saved.getStatus()).isEqualTo("CALCULATED");
}
@Test
@DisplayName("createLedger: clawback null이면 0으로 처리 → net = support_amount")
void createLedger_noClawback() {
SettlingSupportLedgerSaveReq req = new SettlingSupportLedgerSaveReq();
req.setAgentId(1L);
req.setSettleMonth("202506");
req.setSupportAmount(new BigDecimal("300000"));
req.setClawbackAmount(null);
ArgumentCaptor<SettlingSupportLedgerVO> captor = ArgumentCaptor.forClass(SettlingSupportLedgerVO.class);
when(ledgerMapper.insert(captor.capture())).thenReturn(1);
service.createLedger(req);
SettlingSupportLedgerVO saved = captor.getValue();
assertThat(saved.getNetAmount()).isEqualByComparingTo("300000.00");
assertThat(saved.getClawbackAmount()).isEqualByComparingTo("0");
}
}
@@ -4,6 +4,15 @@ import com.ga.batch.settlement.SettlementContext;
import com.ga.batch.settlement.domain.BatchTaxCalculator; import com.ga.batch.settlement.domain.BatchTaxCalculator;
import com.ga.batch.settlement.domain.TaxBreakdown; import com.ga.batch.settlement.domain.TaxBreakdown;
import com.ga.common.util.MoneyUtil; import com.ga.common.util.MoneyUtil;
import com.ga.core.mapper.commission.AwardLedgerMapper;
import com.ga.core.mapper.commission.BranchMgrAllowanceLedgerMapper;
import com.ga.core.mapper.commission.CollectionCommissionLedgerMapper;
import com.ga.core.mapper.commission.LifeSupportLedgerMapper;
import com.ga.core.mapper.commission.PersistencyBonusMapper;
import com.ga.core.mapper.commission.ReferralCommissionLedgerMapper;
import com.ga.core.mapper.commission.ReinstatementCommissionLedgerMapper;
import com.ga.core.mapper.commission.RenewalCommissionLedgerMapper;
import com.ga.core.mapper.commission.SettlingSupportLedgerMapper;
import com.ga.core.mapper.ledger.ExceptionLedgerMapper; import com.ga.core.mapper.ledger.ExceptionLedgerMapper;
import com.ga.core.mapper.ledger.MaintainLedgerMapper; import com.ga.core.mapper.ledger.MaintainLedgerMapper;
import com.ga.core.mapper.ledger.RecruitLedgerMapper; import com.ga.core.mapper.ledger.RecruitLedgerMapper;
@@ -68,6 +77,17 @@ public class AggregateStep implements Tasklet {
private final CommissionStatementMapper statementMapper; private final CommissionStatementMapper statementMapper;
private final BatchTaxCalculator taxCalculator; private final BatchTaxCalculator taxCalculator;
// 설계사 지급성 9종 원장 Mapper
private final CollectionCommissionLedgerMapper collectionMapper;
private final RenewalCommissionLedgerMapper renewalMapper;
private final ReinstatementCommissionLedgerMapper reinstatementMapper;
private final PersistencyBonusMapper persistencyBonusMapper;
private final ReferralCommissionLedgerMapper referralMapper;
private final SettlingSupportLedgerMapper settlingSupportMapper;
private final AwardLedgerMapper awardMapper;
private final LifeSupportLedgerMapper lifeSupportMapper;
private final BranchMgrAllowanceLedgerMapper branchMgrAllowanceMapper;
@Override @Override
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
log.info("[Step8 aggregate] settleMonth={}", ctx.getSettleMonth()); log.info("[Step8 aggregate] settleMonth={}", ctx.getSettleMonth());
@@ -86,6 +106,17 @@ public class AggregateStep implements Tasklet {
excMinus.put(agentId, toBd(row.get("minus"))); excMinus.put(agentId, toBd(row.get("minus")));
} }
// 설계사 지급성 9종 집계
Map<Long, BigDecimal> collectionMap = toMap(collectionMapper.aggregateByAgent(ctx.getSettleMonth()));
Map<Long, BigDecimal> renewalMap = toMap(renewalMapper.aggregateByAgent(ctx.getSettleMonth()));
Map<Long, BigDecimal> reinstatementMap = toMap(reinstatementMapper.aggregateByAgent(ctx.getSettleMonth()));
Map<Long, BigDecimal> persistencyMap = toMap(persistencyBonusMapper.aggregateByAgent(ctx.getSettleMonth()));
Map<Long, BigDecimal> referralMap = toMap(referralMapper.aggregateByAgent(ctx.getSettleMonth()));
Map<Long, BigDecimal> settlingSupportMap = toMap(settlingSupportMapper.aggregateByAgent(ctx.getSettleMonth()));
Map<Long, BigDecimal> awardMap = toMap(awardMapper.aggregateByAgent(ctx.getSettleMonth()));
Map<Long, BigDecimal> lifeSupportMap = toMap(lifeSupportMapper.aggregateByAgent(ctx.getSettleMonth()));
Map<Long, BigDecimal> branchMgrMap = toMap(branchMgrAllowanceMapper.aggregateByAgent(ctx.getSettleMonth()));
// 합집합 agent_id // 합집합 agent_id
Set<Long> agentIds = new HashSet<>(); Set<Long> agentIds = new HashSet<>();
agentIds.addAll(recruitMap.keySet()); agentIds.addAll(recruitMap.keySet());
@@ -94,6 +125,15 @@ public class AggregateStep implements Tasklet {
agentIds.addAll(overrideMap.keySet()); agentIds.addAll(overrideMap.keySet());
agentIds.addAll(excPlus.keySet()); agentIds.addAll(excPlus.keySet());
agentIds.addAll(excMinus.keySet()); agentIds.addAll(excMinus.keySet());
agentIds.addAll(collectionMap.keySet());
agentIds.addAll(renewalMap.keySet());
agentIds.addAll(reinstatementMap.keySet());
agentIds.addAll(persistencyMap.keySet());
agentIds.addAll(referralMap.keySet());
agentIds.addAll(settlingSupportMap.keySet());
agentIds.addAll(awardMap.keySet());
agentIds.addAll(lifeSupportMap.keySet());
agentIds.addAll(branchMgrMap.keySet());
// 활성 설계사 누락 보충 // 활성 설계사 누락 보충
AgentSearchParam ap = new AgentSearchParam(); AgentSearchParam ap = new AgentSearchParam();
@@ -122,8 +162,21 @@ public class AggregateStep implements Tasklet {
BigDecimal ep = excPlus.getOrDefault(agentId, BigDecimal.ZERO); BigDecimal ep = excPlus.getOrDefault(agentId, BigDecimal.ZERO);
BigDecimal em = excMinus.getOrDefault(agentId, BigDecimal.ZERO); BigDecimal em = excMinus.getOrDefault(agentId, BigDecimal.ZERO);
// 설계사 지급성 9종 합산 (원장 없으면 ZERO gross 불변 보장)
BigDecimal otherCommission = BigDecimal.ZERO;
otherCommission = MoneyUtil.add(otherCommission, collectionMap.getOrDefault(agentId, BigDecimal.ZERO));
otherCommission = MoneyUtil.add(otherCommission, renewalMap.getOrDefault(agentId, BigDecimal.ZERO));
otherCommission = MoneyUtil.add(otherCommission, reinstatementMap.getOrDefault(agentId, BigDecimal.ZERO));
otherCommission = MoneyUtil.add(otherCommission, persistencyMap.getOrDefault(agentId, BigDecimal.ZERO));
otherCommission = MoneyUtil.add(otherCommission, referralMap.getOrDefault(agentId, BigDecimal.ZERO));
otherCommission = MoneyUtil.add(otherCommission, settlingSupportMap.getOrDefault(agentId, BigDecimal.ZERO));
otherCommission = MoneyUtil.add(otherCommission, awardMap.getOrDefault(agentId, BigDecimal.ZERO));
otherCommission = MoneyUtil.add(otherCommission, lifeSupportMap.getOrDefault(agentId, BigDecimal.ZERO));
otherCommission = MoneyUtil.add(otherCommission, branchMgrMap.getOrDefault(agentId, BigDecimal.ZERO));
BigDecimal gross = MoneyUtil.add(MoneyUtil.add(MoneyUtil.add(recruit, maintain), ov), ep) BigDecimal gross = MoneyUtil.add(MoneyUtil.add(MoneyUtil.add(recruit, maintain), ov), ep)
.subtract(cb).subtract(em); .subtract(cb).subtract(em);
gross = MoneyUtil.add(gross, otherCommission);
// 세금유형 조회 (없으면 기본: BUSINESS_INCOME / NONE) // 세금유형 조회 (없으면 기본: BUSINESS_INCOME / NONE)
AgentVO agentVO = agentMapper.selectById(agentId); AgentVO agentVO = agentMapper.selectById(agentId);
@@ -152,6 +205,7 @@ public class AggregateStep implements Tasklet {
vo.setExceptionMinus(em); vo.setExceptionMinus(em);
vo.setOverrideTotal(ov); vo.setOverrideTotal(ov);
vo.setChargebackTotal(cb); vo.setChargebackTotal(cb);
vo.setOtherCommissionTotal(otherCommission);
vo.setGrossAmount(gross); vo.setGrossAmount(gross);
vo.setTaxAmount(totalTax); // incomeTax + localTax 합산 vo.setTaxAmount(totalTax); // incomeTax + localTax 합산
vo.setNetAmount(netAmount); // gross - incomeTax - localTax + vat - deduction vo.setNetAmount(netAmount); // gross - incomeTax - localTax + vat - deduction
@@ -0,0 +1,26 @@
-- V100: 공동계약 (joint_contract)
-- 한 계약을 여러 설계사가 공동 모집 → 지분율(share_rate)로 수수료 배분.
-- 공식: 설계사몫 = 계약수수료 × share_rate
-- 검증: 계약별 share_rate 합=1 — Service 가드
CREATE TABLE joint_contract (
joint_id BIGSERIAL PRIMARY KEY,
contract_id INT NOT NULL REFERENCES contract(contract_id),
agent_id INT NOT NULL REFERENCES agent(agent_id),
role VARCHAR(10) NOT NULL,
share_rate NUMERIC(7,4) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_by BIGINT,
UNIQUE (contract_id, agent_id),
CHECK (role IN ('LEAD','SUB')),
CHECK (share_rate >= 0 AND share_rate <= 1)
);
COMMENT ON TABLE joint_contract IS '공동계약 — 복수 설계사의 계약별 지분율 관리';
COMMENT ON COLUMN joint_contract.contract_id IS '대상 계약 FK';
COMMENT ON COLUMN joint_contract.agent_id IS '참여 설계사 FK';
COMMENT ON COLUMN joint_contract.role IS '역할 (LEAD=주설계사/SUB=공동설계사)';
COMMENT ON COLUMN joint_contract.share_rate IS '지분율 (0~1, 계약별 합=1 Service 검증)';
CREATE INDEX idx_jcon_contract ON joint_contract(contract_id);
CREATE INDEX idx_jcon_agent ON joint_contract(agent_id);
@@ -0,0 +1,28 @@
-- V101: 보증보험 (agent_guarantee_insurance)
-- 설계사가 가입하는 신원보증보험 가입현황 추적(관리 목적, 공식 없음).
CREATE TABLE agent_guarantee_insurance (
guarantee_id BIGSERIAL PRIMARY KEY,
agent_id INT NOT NULL REFERENCES agent(agent_id),
policy_no VARCHAR(50) NOT NULL,
carrier_id INT REFERENCES insurance_company(company_id),
guarantee_amount NUMERIC(15,2) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP,
CHECK (status IN ('ACTIVE','EXPIRED','CANCELLED'))
);
COMMENT ON TABLE agent_guarantee_insurance IS '보증보험 — 설계사 신원보증보험 가입현황';
COMMENT ON COLUMN agent_guarantee_insurance.agent_id IS '설계사 FK';
COMMENT ON COLUMN agent_guarantee_insurance.policy_no IS '증권번호';
COMMENT ON COLUMN agent_guarantee_insurance.carrier_id IS '보증보험사 FK(insurance_company)';
COMMENT ON COLUMN agent_guarantee_insurance.guarantee_amount IS '보증금액';
COMMENT ON COLUMN agent_guarantee_insurance.start_date IS '보증 시작일';
COMMENT ON COLUMN agent_guarantee_insurance.end_date IS '보증 종료일';
COMMENT ON COLUMN agent_guarantee_insurance.status IS '상태 (ACTIVE=유효/EXPIRED=만료/CANCELLED=해지)';
CREATE INDEX idx_agi_agent_status ON agent_guarantee_insurance(agent_id, status);
CREATE INDEX idx_agi_end_date ON agent_guarantee_insurance(end_date);
@@ -0,0 +1,24 @@
-- V102: 적립금 (agent_reserve_fund)
-- 환수 대비 수수료 일부 적립 + 잔액 관리.
-- 공식: balance = (직전월 balance) + reserve_in - reserve_out
-- Service가 직전월 balance를 조회하여 누적 계산.
CREATE TABLE agent_reserve_fund (
reserve_id BIGSERIAL PRIMARY KEY,
agent_id INT NOT NULL REFERENCES agent(agent_id),
settle_month CHAR(6) NOT NULL,
reserve_in NUMERIC(15,2) NOT NULL DEFAULT 0,
reserve_out NUMERIC(15,2) NOT NULL DEFAULT 0,
balance NUMERIC(15,2) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (agent_id, settle_month)
);
COMMENT ON TABLE agent_reserve_fund IS '적립금 — 환수 대비 설계사별 월별 적립금 잔액';
COMMENT ON COLUMN agent_reserve_fund.agent_id IS '설계사 FK';
COMMENT ON COLUMN agent_reserve_fund.settle_month IS '정산 귀속월 (YYYYMM)';
COMMENT ON COLUMN agent_reserve_fund.reserve_in IS '당월 적립액';
COMMENT ON COLUMN agent_reserve_fund.reserve_out IS '당월 인출/사용액';
COMMENT ON COLUMN agent_reserve_fund.balance IS '잔액 = 전월잔액 + reserve_in - reserve_out';
CREATE INDEX idx_arf_agent_month ON agent_reserve_fund(agent_id, settle_month);
@@ -0,0 +1,39 @@
-- V103: 수수료 시뮬레이터 (commission_simulation)
-- 보험료/요율/지급률 입력 → 수수료·세금·실지급·예상환수 산출. 시나리오 저장.
-- 공식:
-- company_payment = premium_amount × company_rate
-- agent_commission = company_payment × payout_rate
-- withholding_tax = agent_commission × 0.033 (3.3%, HALF_UP)
-- net_amount = agent_commission - withholding_tax
CREATE TABLE commission_simulation (
sim_id BIGSERIAL PRIMARY KEY,
sim_name VARCHAR(100) NOT NULL,
premium_amount NUMERIC(15,2) NOT NULL,
company_rate NUMERIC(7,4) NOT NULL,
payout_rate NUMERIC(7,4) NOT NULL,
commission_year INT NOT NULL,
company_payment NUMERIC(15,2) NOT NULL,
agent_commission NUMERIC(15,2) NOT NULL,
withholding_tax NUMERIC(15,2) NOT NULL,
net_amount NUMERIC(15,2) NOT NULL,
expected_chargeback NUMERIC(15,2),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_by BIGINT
);
COMMENT ON TABLE commission_simulation IS '수수료 시뮬레이터 — 시나리오별 수수료 산출 결과 저장';
COMMENT ON COLUMN commission_simulation.sim_name IS '시나리오명';
COMMENT ON COLUMN commission_simulation.premium_amount IS '입력 보험료';
COMMENT ON COLUMN commission_simulation.company_rate IS '보험사 요율 (소수)';
COMMENT ON COLUMN commission_simulation.payout_rate IS '지급률 (소수)';
COMMENT ON COLUMN commission_simulation.commission_year IS '회차';
COMMENT ON COLUMN commission_simulation.company_payment IS '보험사 지급액 = 보험료 × company_rate';
COMMENT ON COLUMN commission_simulation.agent_commission IS '설계사 수수료 = 보험사지급액 × payout_rate';
COMMENT ON COLUMN commission_simulation.withholding_tax IS '원천세 = 설계사수수료 × 3.3%';
COMMENT ON COLUMN commission_simulation.net_amount IS '실지급 = 설계사수수료 - 원천세';
COMMENT ON COLUMN commission_simulation.expected_chargeback IS '예상 환수액 (13개월 50% 등 시나리오)';
COMMENT ON COLUMN commission_simulation.created_by IS '시뮬레이션 요청자 FK(users)';
CREATE INDEX idx_simu_created_by ON commission_simulation(created_by, created_at);
CREATE INDEX idx_simu_created_at ON commission_simulation(created_at DESC);
@@ -0,0 +1,153 @@
-- V104: incar 갭 운영 도메인 7개 PAGE 메뉴 등록 (V95 패턴 동일)
-- GRP_SETTLE 하위: STEP_CLOSE, HQ_SETTLE
-- GRP_ORG 하위: GRADE_EVAL, GUARANTEE_INS
-- GRP_PRD 하위: JOINT_CONTRACT
-- GRP_COMMISSION 하위: RESERVE_FUND, COMM_SIMULATOR
--
-- 권한:
-- STEP_CLOSE → READ/UPDATE/APPROVE
-- 나머지 6개 → READ/CREATE/UPDATE/DELETE/EXPORT
-- 역할:
-- SUPER_ADMIN/ADMIN → 전체 권한
-- MANAGER → READ/CREATE/UPDATE/EXPORT (+ APPROVE for STEP_CLOSE, DELETE 제외)
-- AGENT → 본인 관련 도메인 READ (RESERVE_FUND, GUARANTEE_INS, GRADE_EVAL)
-- ============================================================
-- 1. GRP_SETTLE 하위: STEP_CLOSE, HQ_SETTLE
-- ============================================================
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
('STEP_CLOSE', '단계마감', '/settle/step-close', 'settle/StepClose', 90),
('HQ_SETTLE', '본사정산', '/settle/hq-settle', 'settle/HqSettle', 100)
) 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);
-- ============================================================
-- 2. GRP_ORG 하위: GRADE_EVAL, GUARANTEE_INS
-- ============================================================
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
('GRADE_EVAL', '등급평가', '/org/grade-eval', 'org/GradeEvaluation', 60),
('GUARANTEE_INS', '보증보험', '/org/guarantee', 'org/GuaranteeInsurance', 70)
) AS x(code, name, path, cmpt, sort)
ON p.menu_code = 'GRP_ORG'
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code);
-- ============================================================
-- 3. GRP_PRD 하위: JOINT_CONTRACT
-- ============================================================
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
('JOINT_CONTRACT', '공동계약', '/contracts/joint', 'contracts/JointContract', 60)
) AS x(code, name, path, cmpt, sort)
ON p.menu_code = 'GRP_PRD'
WHERE NOT EXISTS (SELECT 1 FROM menu WHERE menu_code = x.code);
-- ============================================================
-- 4. GRP_COMMISSION 하위: RESERVE_FUND, COMM_SIMULATOR
-- ============================================================
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
('RESERVE_FUND', '적립금', '/commission/reserve', 'commission/ReserveFund', 150),
('COMM_SIMULATOR', '수수료 시뮬레이터', '/commission/simulator', 'commission/CommissionSimulator', 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);
-- ============================================================
-- 5. STEP_CLOSE 권한 등록 (READ/UPDATE/APPROVE)
-- ============================================================
INSERT INTO menu_permission (menu_id, perm_code, perm_name, sort_order)
SELECT m.menu_id, p.code, p.name, p.sort
FROM menu m
CROSS JOIN (VALUES
('READ', '조회', 10),
('UPDATE', '수정', 30),
('APPROVE', '승인', 50)
) AS p(code, name, sort)
WHERE m.menu_code = 'STEP_CLOSE'
ON CONFLICT (menu_id, perm_code) DO NOTHING;
-- ============================================================
-- 6. 나머지 6개 메뉴 권한 등록 (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 IN ('HQ_SETTLE','GRADE_EVAL','JOINT_CONTRACT','GUARANTEE_INS','RESERVE_FUND','COMM_SIMULATOR')
ON CONFLICT (menu_id, perm_code) DO NOTHING;
-- ============================================================
-- 7. SUPER_ADMIN / ADMIN — 7개 메뉴 전체 권한
-- ============================================================
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 IN ('STEP_CLOSE','HQ_SETTLE','GRADE_EVAL','JOINT_CONTRACT','GUARANTEE_INS','RESERVE_FUND','COMM_SIMULATOR')
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
-- ============================================================
-- 8. MANAGER — READ/CREATE/UPDATE/EXPORT + APPROVE(STEP_CLOSE)
-- ============================================================
-- STEP_CLOSE: READ/UPDATE/APPROVE
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','UPDATE','APPROVE')
AND m.menu_code = 'STEP_CLOSE'
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
-- 나머지 6개: READ/CREATE/UPDATE/EXPORT (DELETE 제외)
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 IN ('HQ_SETTLE','GRADE_EVAL','JOINT_CONTRACT','GUARANTEE_INS','RESERVE_FUND','COMM_SIMULATOR')
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
-- ============================================================
-- 9. AGENT — 본인 관련 도메인 READ (RESERVE_FUND, GUARANTEE_INS, GRADE_EVAL)
-- ============================================================
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 IN ('RESERVE_FUND','GUARANTEE_INS','GRADE_EVAL')
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
@@ -0,0 +1,37 @@
-- V105: incar 갭 운영 도메인 공통코드 6종 (V96 패턴 동일)
-- STEP_CODE / STEP_STATUS / HQ_ITEM_TYPE / JOINT_ROLE / GUARANTEE_STATUS / GRADE_EVAL_STATUS
-- ===== 그룹 =====
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
('STEP_CODE', '단계코드', 'RECEIVE=수신마감/CALC=계산마감/VERIFY=검증마감/PAY=지급마감', 'Y', 490),
('STEP_STATUS', '단계상태', 'OPEN=열림/CLOSED=마감', 'Y', 500),
('HQ_ITEM_TYPE', '본사정산유형', 'INCOME=수입/EXPENSE=비용', 'Y', 510),
('JOINT_ROLE', '공동계약역할', 'LEAD=주설계사/SUB=공동설계사', 'Y', 520),
('GUARANTEE_STATUS', '보증보험상태', 'ACTIVE=유효/EXPIRED=만료/CANCELLED=해지', 'Y', 530),
('GRADE_EVAL_STATUS', '등급평가상태', 'EVALUATED=평가완료/APPLIED=반영완료', 'Y', 540)
ON CONFLICT (group_code) DO NOTHING;
-- ===== 상세 =====
INSERT INTO common_code (group_code, code, code_name, sort_order) VALUES
-- STEP_CODE
('STEP_CODE', 'RECEIVE', '수신마감', 10),
('STEP_CODE', 'CALC', '계산마감', 20),
('STEP_CODE', 'VERIFY', '검증마감', 30),
('STEP_CODE', 'PAY', '지급마감', 40),
-- STEP_STATUS
('STEP_STATUS', 'OPEN', '열림', 10),
('STEP_STATUS', 'CLOSED', '마감', 20),
-- HQ_ITEM_TYPE
('HQ_ITEM_TYPE', 'INCOME', '수입', 10),
('HQ_ITEM_TYPE', 'EXPENSE', '비용', 20),
-- JOINT_ROLE
('JOINT_ROLE', 'LEAD', '주설계사', 10),
('JOINT_ROLE', 'SUB', '공동설계사', 20),
-- GUARANTEE_STATUS
('GUARANTEE_STATUS', 'ACTIVE', '유효', 10),
('GUARANTEE_STATUS', 'EXPIRED', '만료', 20),
('GUARANTEE_STATUS', 'CANCELLED', '해지', 30),
-- GRADE_EVAL_STATUS
('GRADE_EVAL_STATUS', 'EVALUATED', '평가완료', 10),
('GRADE_EVAL_STATUS', 'APPLIED', '반영완료', 20)
ON CONFLICT (group_code, code) DO NOTHING;
@@ -0,0 +1,9 @@
-- V106: settle_master에 기타수수료 합계 컬럼 추가
-- 설계사 지급성 9종(수금·갱신·부활·유지보너스·소개·정착지원·시상·생보운영·지점장수당) 집계용
-- 기존 행은 DEFAULT 0으로 백필되므로 기존 정산 결과 불변.
ALTER TABLE settle_master
ADD COLUMN other_commission_total NUMERIC(15,2) NOT NULL DEFAULT 0;
COMMENT ON COLUMN settle_master.other_commission_total
IS '기타수수료 합계(수금+갱신+부활+유지보너스+소개+정착지원+시상+생보운영+지점장수당, status!=CANCELLED)';
@@ -0,0 +1,81 @@
-- V88: 수입수수료 + 수지차 (income_commission_ledger + margin_summary)
-- 보험사가 GA에 지급한 수입 측 원장. 수지차 = 수입 합계 - 지급 합계(settle_master 기준).
--
-- settle_master 지급 합계 집계 컬럼 (확인 완료):
-- settle_master.agent_id : 설계사 FK (BIGINT)
-- settle_master.settle_month : 정산월 CHAR(6)
-- settle_master.net_amount : 설계사 실지급액 (세후) NUMERIC
-- settle_master.gross_amount : 설계사 총지급액 (세전) NUMERIC
-- payout_total 산출 시 net_amount 합산 권장 (tax_amount 차감 후 기준)
-- ============================================================
-- 1. income_commission_ledger — 수입수수료 원장
-- 보험사 → GA 수입 수수료. 계약 단위로 발생.
-- ============================================================
CREATE TABLE income_commission_ledger (
income_id BIGSERIAL PRIMARY KEY,
contract_id BIGINT NOT NULL REFERENCES contract(contract_id),
carrier_id BIGINT NOT NULL REFERENCES insurance_company(company_id),
settle_month CHAR(6) NOT NULL,
premium_amount NUMERIC(15,2) NOT NULL,
company_rate NUMERIC(7,4) NOT NULL,
income_amount NUMERIC(15,2) NOT NULL,
commission_type VARCHAR(20) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP,
CHECK (commission_type IN ('RECRUIT','MAINTAIN','RENEWAL','REINSTATE','COLLECTION')),
CHECK (status IN ('CALCULATED','CONFIRMED','CANCELLED')),
CHECK (company_rate >= 0 AND company_rate <= 1),
CHECK (premium_amount >= 0),
CHECK (income_amount >= 0)
);
COMMENT ON TABLE income_commission_ledger IS '수입수수료 원장 — 보험사가 GA에 지급한 수입 수수료 (계약 단위)';
COMMENT ON COLUMN income_commission_ledger.income_id IS '수입수수료 원장 PK';
COMMENT ON COLUMN income_commission_ledger.contract_id IS '대상 계약 FK';
COMMENT ON COLUMN income_commission_ledger.carrier_id IS '보험사 FK (insurance_company.company_id)';
COMMENT ON COLUMN income_commission_ledger.settle_month IS '정산 귀속월 (YYYYMM)';
COMMENT ON COLUMN income_commission_ledger.premium_amount IS '보험료';
COMMENT ON COLUMN income_commission_ledger.company_rate IS '보험사 수수료율 (소수, 예: 0.12 = 12%)';
COMMENT ON COLUMN income_commission_ledger.income_amount IS '수입수수료 = premium_amount × company_rate';
COMMENT ON COLUMN income_commission_ledger.commission_type IS '수수료 유형 (RECRUIT/MAINTAIN/RENEWAL/REINSTATE/COLLECTION)';
COMMENT ON COLUMN income_commission_ledger.status IS '상태 (CALCULATED/CONFIRMED/CANCELLED)';
CREATE INDEX idx_icl_carrier_month ON income_commission_ledger(carrier_id, settle_month);
CREATE INDEX idx_icl_contract ON income_commission_ledger(contract_id);
CREATE INDEX idx_icl_status_month ON income_commission_ledger(status, settle_month);
-- ============================================================
-- 2. margin_summary — 수지차 집계
-- 수지차 recalc 배치 액션으로 채움.
-- agent_id NULL = 전체(GA 총 수지차).
-- payout_total: settle_master.net_amount SUM (설계사별 세후 실지급액 기준)
-- ============================================================
CREATE TABLE margin_summary (
margin_id BIGSERIAL PRIMARY KEY,
settle_month CHAR(6) NOT NULL,
agent_id BIGINT REFERENCES agent(agent_id),
income_total NUMERIC(15,2) NOT NULL DEFAULT 0,
payout_total NUMERIC(15,2) NOT NULL DEFAULT 0,
margin_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
margin_rate NUMERIC(7,4),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (settle_month, agent_id),
CHECK (income_total >= 0),
CHECK (payout_total >= 0)
);
COMMENT ON TABLE margin_summary IS '수지차 집계 — settle_month + agent_id 단위 수입/지급 대비 수지차';
COMMENT ON COLUMN margin_summary.margin_id IS '수지차 집계 PK';
COMMENT ON COLUMN margin_summary.settle_month IS '정산월 (YYYYMM)';
COMMENT ON COLUMN margin_summary.agent_id IS '설계사 FK (NULL = GA 전체 합산)';
COMMENT ON COLUMN margin_summary.income_total IS '수입 합계 (income_commission_ledger SUM)';
COMMENT ON COLUMN margin_summary.payout_total IS '지급 합계 (settle_master.net_amount SUM — 설계사 세후 실지급액)';
COMMENT ON COLUMN margin_summary.margin_amount IS '수지차 = income_total - payout_total';
COMMENT ON COLUMN margin_summary.margin_rate IS '수지차율 = margin_amount / income_total (income_total > 0 일 때)';
CREATE INDEX idx_ms_month ON margin_summary(settle_month);
CREATE INDEX idx_ms_agent_month ON margin_summary(agent_id, settle_month);
@@ -0,0 +1,72 @@
-- V89: 소개·이관수수료 (referral_commission_rule + referral_commission_ledger)
-- 계약을 소개(REFERRAL)하거나 이관(TRANSFER)한 설계사에게 지급하는 수수료.
-- 정률(RATE): commission_amount = base_amount × commission_rate
-- 정액(FIXED): commission_amount = fixed_amount
-- ============================================================
-- 1. referral_commission_rule — 소개·이관수수료 룰 마스터
-- ============================================================
CREATE TABLE referral_commission_rule (
rule_id BIGSERIAL PRIMARY KEY,
referral_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,
CHECK (referral_type IN ('REFERRAL','TRANSFER')),
CHECK (calc_type IN ('RATE','FIXED')),
CHECK (commission_rate IS NULL OR (commission_rate >= 0 AND commission_rate <= 1)),
CHECK (fixed_amount IS NULL OR fixed_amount >= 0)
);
COMMENT ON TABLE referral_commission_rule IS '소개·이관수수료 룰 마스터 — 소개/이관 유형별 수수료율 또는 정액 정의';
COMMENT ON COLUMN referral_commission_rule.rule_id IS '룰 PK';
COMMENT ON COLUMN referral_commission_rule.referral_type IS '유형 (REFERRAL=소개 / TRANSFER=이관)';
COMMENT ON COLUMN referral_commission_rule.calc_type IS '계산방식 (RATE=정률 / FIXED=정액)';
COMMENT ON COLUMN referral_commission_rule.commission_rate IS '정률 요율 (소수, calc_type=RATE 일 때 사용)';
COMMENT ON COLUMN referral_commission_rule.fixed_amount IS '정액 금액 (calc_type=FIXED 일 때 사용)';
COMMENT ON COLUMN referral_commission_rule.effective_from IS '적용 시작일';
COMMENT ON COLUMN referral_commission_rule.effective_to IS '적용 종료일 (NULL=현재 유효)';
CREATE INDEX idx_rfcr_type_from ON referral_commission_rule(referral_type, effective_from);
-- ============================================================
-- 2. referral_commission_ledger — 소개·이관수수료 원장
-- ============================================================
CREATE TABLE referral_commission_ledger (
ledger_id BIGSERIAL PRIMARY KEY,
contract_id BIGINT NOT NULL REFERENCES contract(contract_id),
referrer_agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
settle_month CHAR(6) NOT NULL,
referral_type VARCHAR(20) NOT NULL,
base_amount NUMERIC(15,2) NOT NULL,
commission_rate NUMERIC(7,4),
commission_amount NUMERIC(15,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP,
CHECK (referral_type IN ('REFERRAL','TRANSFER')),
CHECK (status IN ('CALCULATED','CONFIRMED','CANCELLED')),
CHECK (base_amount >= 0),
CHECK (commission_amount >= 0)
);
COMMENT ON TABLE referral_commission_ledger IS '소개·이관수수료 원장 — 소개/이관 설계사에게 지급하는 수수료 발생 이력';
COMMENT ON COLUMN referral_commission_ledger.ledger_id IS '원장 PK';
COMMENT ON COLUMN referral_commission_ledger.contract_id IS '대상 계약 FK';
COMMENT ON COLUMN referral_commission_ledger.referrer_agent_id IS '소개/이관한 설계사 FK';
COMMENT ON COLUMN referral_commission_ledger.settle_month IS '정산 귀속월 (YYYYMM)';
COMMENT ON COLUMN referral_commission_ledger.referral_type IS '유형 (REFERRAL=소개 / TRANSFER=이관)';
COMMENT ON COLUMN referral_commission_ledger.base_amount IS '기준금액 (수수료 또는 보험료)';
COMMENT ON COLUMN referral_commission_ledger.commission_rate IS '적용 정률 요율 (RATE 방식 시)';
COMMENT ON COLUMN referral_commission_ledger.commission_amount IS '수수료액 = base×rate (RATE) 또는 fixed_amount (FIXED)';
COMMENT ON COLUMN referral_commission_ledger.status IS '상태 (CALCULATED/CONFIRMED/CANCELLED)';
CREATE INDEX idx_rfcl_agent_month ON referral_commission_ledger(referrer_agent_id, settle_month);
CREATE INDEX idx_rfcl_contract ON referral_commission_ledger(contract_id);
@@ -0,0 +1,65 @@
-- V90: 정착지원금 (settling_support_rule + settling_support_ledger)
-- 신입/경력 설계사 정착 지원금. 등급별 정액 월 지급 + 조기 해촉 시 환수.
-- 공식: net_amount = support_amount - clawback_amount
-- 환수: 조기해촉 시 clawback_amount = support_amount × (잔여약정개월 / 약정개월)
-- ============================================================
-- 1. settling_support_rule — 정착지원금 룰 마스터
-- ============================================================
CREATE TABLE settling_support_rule (
rule_id BIGSERIAL PRIMARY KEY,
agent_grade VARCHAR(20) NOT NULL,
support_amount NUMERIC(15,2) NOT NULL,
duration_months INT NOT NULL,
clawback_months INT NOT NULL,
effective_from DATE NOT NULL,
effective_to DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_by BIGINT,
updated_at TIMESTAMP,
CHECK (support_amount >= 0),
CHECK (duration_months > 0),
CHECK (clawback_months >= 0)
);
COMMENT ON TABLE settling_support_rule IS '정착지원금 룰 마스터 — 설계사 등급별 월 정착지원금 및 약정 기간 정의';
COMMENT ON COLUMN settling_support_rule.rule_id IS '룰 PK';
COMMENT ON COLUMN settling_support_rule.agent_grade IS '설계사 등급';
COMMENT ON COLUMN settling_support_rule.support_amount IS '월 정액 지원금';
COMMENT ON COLUMN settling_support_rule.duration_months IS '약정 지원 개월수';
COMMENT ON COLUMN settling_support_rule.clawback_months IS '이 개월 내 해촉 시 환수 대상 (0=환수 없음)';
COMMENT ON COLUMN settling_support_rule.effective_from IS '적용 시작일';
COMMENT ON COLUMN settling_support_rule.effective_to IS '적용 종료일 (NULL=현재 유효)';
CREATE INDEX idx_ssr_grade_from ON settling_support_rule(agent_grade, effective_from);
-- ============================================================
-- 2. settling_support_ledger — 정착지원금 원장
-- ============================================================
CREATE TABLE settling_support_ledger (
ledger_id BIGSERIAL PRIMARY KEY,
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
settle_month CHAR(6) NOT NULL,
support_amount NUMERIC(15,2) NOT NULL,
clawback_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
net_amount NUMERIC(15,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP,
CHECK (status IN ('CALCULATED','PAID','CLAWED_BACK','CANCELLED')),
CHECK (support_amount >= 0),
CHECK (clawback_amount >= 0)
);
COMMENT ON TABLE settling_support_ledger IS '정착지원금 원장 — 설계사별 월 정착지원금 지급/환수 이력';
COMMENT ON COLUMN settling_support_ledger.ledger_id IS '원장 PK';
COMMENT ON COLUMN settling_support_ledger.agent_id IS '대상 설계사 FK';
COMMENT ON COLUMN settling_support_ledger.settle_month IS '정산 귀속월 (YYYYMM)';
COMMENT ON COLUMN settling_support_ledger.support_amount IS '지급액 (등급별 support_amount)';
COMMENT ON COLUMN settling_support_ledger.clawback_amount IS '환수액 = support_amount × (잔여약정개월 / 약정개월) (조기해촉 시)';
COMMENT ON COLUMN settling_support_ledger.net_amount IS '실지급액 = support_amount - clawback_amount';
COMMENT ON COLUMN settling_support_ledger.status IS '상태 (CALCULATED/PAID/CLAWED_BACK/CANCELLED)';
CREATE INDEX idx_ssl_agent_month ON settling_support_ledger(agent_id, settle_month);
@@ -0,0 +1,70 @@
-- V91: 인정실적 + 환산보험료 (product_conversion_rate + recognized_performance_ledger)
-- 환산보험료 = 보험료 × 상품별 환산율.
-- 인정실적 = 환산보험료 × 인정율.
-- 시상/등급 산정의 기초 데이터.
-- ============================================================
-- 1. product_conversion_rate — 상품별 환산율 마스터
-- ============================================================
CREATE TABLE product_conversion_rate (
rate_id BIGSERIAL PRIMARY KEY,
product_id BIGINT NOT NULL REFERENCES product(product_id),
conversion_rate NUMERIC(7,4) NOT NULL,
effective_from DATE NOT NULL,
effective_to DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_by BIGINT,
updated_at TIMESTAMP,
UNIQUE (product_id, effective_from),
CHECK (conversion_rate >= 0 AND conversion_rate <= 1)
);
COMMENT ON TABLE product_conversion_rate IS '상품별 환산율 마스터 — 인정실적 산출을 위한 보험료 환산율';
COMMENT ON COLUMN product_conversion_rate.rate_id IS '환산율 PK';
COMMENT ON COLUMN product_conversion_rate.product_id IS '적용 상품 FK';
COMMENT ON COLUMN product_conversion_rate.conversion_rate IS '환산율 (소수, 예: 0.5 = 50%)';
COMMENT ON COLUMN product_conversion_rate.effective_from IS '적용 시작일';
COMMENT ON COLUMN product_conversion_rate.effective_to IS '적용 종료일 (NULL=현재 유효)';
CREATE INDEX idx_pcr_product_from ON product_conversion_rate(product_id, effective_from);
-- ============================================================
-- 2. recognized_performance_ledger — 인정실적 원장
-- ============================================================
CREATE TABLE recognized_performance_ledger (
ledger_id BIGSERIAL PRIMARY KEY,
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
contract_id BIGINT NOT NULL REFERENCES contract(contract_id),
settle_month CHAR(6) NOT NULL,
premium_amount NUMERIC(15,2) NOT NULL,
conversion_rate NUMERIC(7,4) NOT NULL,
converted_premium NUMERIC(15,2) NOT NULL,
recognition_rate NUMERIC(7,4) NOT NULL,
recognized_amount NUMERIC(15,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP,
CHECK (status IN ('CALCULATED','CONFIRMED','CANCELLED')),
CHECK (premium_amount >= 0),
CHECK (conversion_rate >= 0 AND conversion_rate <= 1),
CHECK (converted_premium >= 0),
CHECK (recognition_rate >= 0 AND recognition_rate <= 1),
CHECK (recognized_amount >= 0)
);
COMMENT ON TABLE recognized_performance_ledger IS '인정실적 원장 — 설계사별 계약 단위 환산보험료 및 인정실적';
COMMENT ON COLUMN recognized_performance_ledger.ledger_id IS '원장 PK';
COMMENT ON COLUMN recognized_performance_ledger.agent_id IS '설계사 FK';
COMMENT ON COLUMN recognized_performance_ledger.contract_id IS '대상 계약 FK';
COMMENT ON COLUMN recognized_performance_ledger.settle_month IS '정산 귀속월 (YYYYMM)';
COMMENT ON COLUMN recognized_performance_ledger.premium_amount IS '보험료';
COMMENT ON COLUMN recognized_performance_ledger.conversion_rate IS '적용 환산율 (product_conversion_rate 스냅샷)';
COMMENT ON COLUMN recognized_performance_ledger.converted_premium IS '환산보험료 = premium_amount × conversion_rate';
COMMENT ON COLUMN recognized_performance_ledger.recognition_rate IS '인정율 (설계사 등급/정책 기준)';
COMMENT ON COLUMN recognized_performance_ledger.recognized_amount IS '인정실적 = converted_premium × recognition_rate';
COMMENT ON COLUMN recognized_performance_ledger.status IS '상태 (CALCULATED/CONFIRMED/CANCELLED)';
CREATE INDEX idx_rpl_agent_month ON recognized_performance_ledger(agent_id, settle_month);
CREATE INDEX idx_rpl_contract ON recognized_performance_ledger(contract_id);
@@ -0,0 +1,71 @@
-- V92: 시상 (award_rule + award_ledger)
-- 신인도입/법인/유지율/실적 등 시상. 조건 충족 시 정률 또는 정액 지급.
-- 정률(RATE): award_amount = base_amount × award_rate
-- 정액(FIXED): award_amount = award_fixed
-- ============================================================
-- 1. award_rule — 시상 룰 마스터
-- ============================================================
CREATE TABLE award_rule (
rule_id BIGSERIAL PRIMARY KEY,
award_type VARCHAR(20) NOT NULL,
calc_type VARCHAR(10) NOT NULL,
threshold_amount NUMERIC(15,2),
award_rate NUMERIC(7,4),
award_fixed NUMERIC(15,2),
effective_from DATE NOT NULL,
effective_to DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_by BIGINT,
updated_at TIMESTAMP,
CHECK (award_type IN ('NEW_RECRUIT','CORPORATE','RETENTION','PERFORMANCE')),
CHECK (calc_type IN ('RATE','FIXED')),
CHECK (award_rate IS NULL OR (award_rate >= 0 AND award_rate <= 1)),
CHECK (award_fixed IS NULL OR award_fixed >= 0),
CHECK (threshold_amount IS NULL OR threshold_amount >= 0)
);
COMMENT ON TABLE award_rule IS '시상 룰 마스터 — 시상 유형별 조건 및 지급 기준';
COMMENT ON COLUMN award_rule.rule_id IS '룰 PK';
COMMENT ON COLUMN award_rule.award_type IS '시상 유형 (NEW_RECRUIT=신인도입/CORPORATE=법인/RETENTION=유지율/PERFORMANCE=실적)';
COMMENT ON COLUMN award_rule.calc_type IS '계산방식 (RATE=정률 / FIXED=정액)';
COMMENT ON COLUMN award_rule.threshold_amount IS '충족 기준 실적 (이상 시 시상 발생, NULL=조건 없음)';
COMMENT ON COLUMN award_rule.award_rate IS '정률 시상율 (소수, calc_type=RATE 일 때 사용)';
COMMENT ON COLUMN award_rule.award_fixed IS '정액 시상금 (calc_type=FIXED 일 때 사용)';
COMMENT ON COLUMN award_rule.effective_from IS '적용 시작일';
COMMENT ON COLUMN award_rule.effective_to IS '적용 종료일 (NULL=현재 유효)';
CREATE INDEX idx_awdr_type_from ON award_rule(award_type, effective_from);
-- ============================================================
-- 2. award_ledger — 시상 원장
-- ============================================================
CREATE TABLE award_ledger (
ledger_id BIGSERIAL PRIMARY KEY,
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
settle_month CHAR(6) NOT NULL,
award_type VARCHAR(20) NOT NULL,
base_amount NUMERIC(15,2) NOT NULL,
award_amount NUMERIC(15,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP,
CHECK (award_type IN ('NEW_RECRUIT','CORPORATE','RETENTION','PERFORMANCE')),
CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CANCELLED')),
CHECK (base_amount >= 0),
CHECK (award_amount >= 0)
);
COMMENT ON TABLE award_ledger IS '시상 원장 — 설계사별 시상 발생 이력';
COMMENT ON COLUMN award_ledger.ledger_id IS '원장 PK';
COMMENT ON COLUMN award_ledger.agent_id IS '설계사 FK';
COMMENT ON COLUMN award_ledger.settle_month IS '정산 귀속월 (YYYYMM)';
COMMENT ON COLUMN award_ledger.award_type IS '시상 유형 (NEW_RECRUIT/CORPORATE/RETENTION/PERFORMANCE)';
COMMENT ON COLUMN award_ledger.base_amount IS '기준 실적 (인정실적 합산 등)';
COMMENT ON COLUMN award_ledger.award_amount IS '시상금 = base×award_rate (RATE) 또는 award_fixed (FIXED)';
COMMENT ON COLUMN award_ledger.status IS '상태 (CALCULATED/CONFIRMED/PAID/CANCELLED)';
CREATE INDEX idx_awdl_agent_month ON award_ledger(agent_id, settle_month);
CREATE INDEX idx_awdl_type_month ON award_ledger(award_type, settle_month);
@@ -0,0 +1,65 @@
-- V93: 생보운영지원수수료 (life_support_rule + life_support_ledger)
-- 생보 계약 운영지원 수수료. 설계사 등급별 지급률 × 생보 실적 + 환수.
-- 공식: support_amount = base_amount × support_rate
-- net_amount = support_amount - clawback_amount
-- ============================================================
-- 1. life_support_rule — 생보운영지원수수료 룰 마스터
-- ============================================================
CREATE TABLE life_support_rule (
rule_id BIGSERIAL PRIMARY KEY,
agent_grade VARCHAR(20) NOT NULL,
support_rate NUMERIC(7,4) NOT NULL,
effective_from DATE NOT NULL,
effective_to DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_by BIGINT,
updated_at TIMESTAMP,
CHECK (support_rate >= 0 AND support_rate <= 1)
);
COMMENT ON TABLE life_support_rule IS '생보운영지원수수료 룰 마스터 — 등급별 생보 운영지원 수수료율';
COMMENT ON COLUMN life_support_rule.rule_id IS '룰 PK';
COMMENT ON COLUMN life_support_rule.agent_grade IS '설계사 등급';
COMMENT ON COLUMN life_support_rule.support_rate IS '지급률 (소수, 예: 0.03 = 3%)';
COMMENT ON COLUMN life_support_rule.effective_from IS '적용 시작일';
COMMENT ON COLUMN life_support_rule.effective_to IS '적용 종료일 (NULL=현재 유효)';
CREATE INDEX idx_lsr_grade_from ON life_support_rule(agent_grade, effective_from);
-- ============================================================
-- 2. life_support_ledger — 생보운영지원수수료 원장
-- ============================================================
CREATE TABLE life_support_ledger (
ledger_id BIGSERIAL PRIMARY KEY,
agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
settle_month CHAR(6) NOT NULL,
base_amount NUMERIC(15,2) NOT NULL,
support_rate NUMERIC(7,4) NOT NULL,
support_amount NUMERIC(15,2) NOT NULL,
clawback_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
net_amount NUMERIC(15,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP,
CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CLAWED_BACK','CANCELLED')),
CHECK (base_amount >= 0),
CHECK (support_rate >= 0 AND support_rate <= 1),
CHECK (support_amount >= 0),
CHECK (clawback_amount >= 0)
);
COMMENT ON TABLE life_support_ledger IS '생보운영지원수수료 원장 — 설계사별 생보 운영지원 수수료 발생/환수 이력';
COMMENT ON COLUMN life_support_ledger.ledger_id IS '원장 PK';
COMMENT ON COLUMN life_support_ledger.agent_id IS '설계사 FK';
COMMENT ON COLUMN life_support_ledger.settle_month IS '정산 귀속월 (YYYYMM)';
COMMENT ON COLUMN life_support_ledger.base_amount IS '생보 실적 기준금액';
COMMENT ON COLUMN life_support_ledger.support_rate IS '적용 지급률 (life_support_rule 스냅샷)';
COMMENT ON COLUMN life_support_ledger.support_amount IS '지원수수료 = base_amount × support_rate';
COMMENT ON COLUMN life_support_ledger.clawback_amount IS '환수액';
COMMENT ON COLUMN life_support_ledger.net_amount IS '실지급액 = support_amount - clawback_amount';
COMMENT ON COLUMN life_support_ledger.status IS '상태 (CALCULATED/CONFIRMED/PAID/CLAWED_BACK/CANCELLED)';
CREATE INDEX idx_lsl_agent_month ON life_support_ledger(agent_id, settle_month);
@@ -0,0 +1,69 @@
-- V94: 지점장수당 (branch_mgr_allowance_rule + branch_mgr_allowance_ledger)
-- 지점장이 소속 조직 실적에 대해 받는 수당. cap 제한 + 환수.
-- 공식: allowance_amount = LEAST(base_amount × allowance_rate, cap_amount)
-- (cap_amount NULL 이면 상한 없음)
-- ============================================================
-- 1. branch_mgr_allowance_rule — 지점장수당 룰 마스터
-- ============================================================
CREATE TABLE branch_mgr_allowance_rule (
rule_id BIGSERIAL PRIMARY KEY,
position_code VARCHAR(20) NOT NULL,
allowance_rate NUMERIC(7,4) NOT NULL,
cap_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,
CHECK (allowance_rate >= 0 AND allowance_rate <= 1),
CHECK (cap_amount IS NULL OR cap_amount >= 0)
);
COMMENT ON TABLE branch_mgr_allowance_rule IS '지점장수당 룰 마스터 — 직책별 수당율 및 상한 정의';
COMMENT ON COLUMN branch_mgr_allowance_rule.rule_id IS '룰 PK';
COMMENT ON COLUMN branch_mgr_allowance_rule.position_code IS '직책 코드 (지점장 등)';
COMMENT ON COLUMN branch_mgr_allowance_rule.allowance_rate IS '수당율 (소수, 예: 0.02 = 2%)';
COMMENT ON COLUMN branch_mgr_allowance_rule.cap_amount IS '수당 상한금액 (NULL=무제한)';
COMMENT ON COLUMN branch_mgr_allowance_rule.effective_from IS '적용 시작일';
COMMENT ON COLUMN branch_mgr_allowance_rule.effective_to IS '적용 종료일 (NULL=현재 유효)';
CREATE INDEX idx_bmar_position_from ON branch_mgr_allowance_rule(position_code, effective_from);
-- ============================================================
-- 2. branch_mgr_allowance_ledger — 지점장수당 원장
-- ============================================================
CREATE TABLE branch_mgr_allowance_ledger (
ledger_id BIGSERIAL PRIMARY KEY,
manager_agent_id BIGINT NOT NULL REFERENCES agent(agent_id),
org_id BIGINT NOT NULL REFERENCES organization(org_id),
settle_month CHAR(6) NOT NULL,
base_amount NUMERIC(15,2) NOT NULL,
allowance_rate NUMERIC(7,4) NOT NULL,
allowance_amount NUMERIC(15,2) NOT NULL,
clawback_amount NUMERIC(15,2) NOT NULL DEFAULT 0,
status VARCHAR(20) NOT NULL DEFAULT 'CALCULATED',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP,
CHECK (status IN ('CALCULATED','CONFIRMED','PAID','CLAWED_BACK','CANCELLED')),
CHECK (base_amount >= 0),
CHECK (allowance_rate >= 0 AND allowance_rate <= 1),
CHECK (allowance_amount >= 0),
CHECK (clawback_amount >= 0)
);
COMMENT ON TABLE branch_mgr_allowance_ledger IS '지점장수당 원장 — 지점장이 소속 조직 실적 기준으로 받는 수당 이력';
COMMENT ON COLUMN branch_mgr_allowance_ledger.ledger_id IS '원장 PK';
COMMENT ON COLUMN branch_mgr_allowance_ledger.manager_agent_id IS '지점장 설계사 FK';
COMMENT ON COLUMN branch_mgr_allowance_ledger.org_id IS '대상 조직 FK';
COMMENT ON COLUMN branch_mgr_allowance_ledger.settle_month IS '정산 귀속월 (YYYYMM)';
COMMENT ON COLUMN branch_mgr_allowance_ledger.base_amount IS '조직 실적 기준금액';
COMMENT ON COLUMN branch_mgr_allowance_ledger.allowance_rate IS '적용 수당율 (branch_mgr_allowance_rule 스냅샷)';
COMMENT ON COLUMN branch_mgr_allowance_ledger.allowance_amount IS '수당액 = LEAST(base_amount × allowance_rate, cap_amount)';
COMMENT ON COLUMN branch_mgr_allowance_ledger.clawback_amount IS '환수액';
COMMENT ON COLUMN branch_mgr_allowance_ledger.status IS '상태 (CALCULATED/CONFIRMED/PAID/CLAWED_BACK/CANCELLED)';
CREATE INDEX idx_bmal_manager_month ON branch_mgr_allowance_ledger(manager_agent_id, settle_month);
CREATE INDEX idx_bmal_org_month ON branch_mgr_allowance_ledger(org_id, settle_month);
@@ -0,0 +1,90 @@
-- V95: incar 갭 수수료 도메인 7개 PAGE 메뉴 등록
-- GRP_COMMISSION 하위 PAGE 7개 추가 (V54 패턴 동일).
-- 권한: READ/CREATE/UPDATE/DELETE/EXPORT
-- 역할: SUPER_ADMIN/ADMIN 전체, MANAGER READ/CREATE/UPDATE/EXPORT,
-- AGENT READ (INCOME_COMM 제외 — 본인 관련 도메인만)
-- ============================================================
-- 1. GRP_COMMISSION 하위 PAGE 7개 (incar P7)
-- ============================================================
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
('INCOME_COMM', '수입수수료/수지차', '/commission/income', 'commission/IncomeCommission', 80),
('REFERRAL_COMM', '소개·이관수수료', '/commission/referral', 'commission/ReferralCommission',90),
('SETTLING_SUPPORT', '정착지원금', '/commission/settling-support', 'commission/SettlingSupport', 100),
('RECOGNIZED_PERF', '인정실적', '/commission/recognized', 'commission/RecognizedPerformance',110),
('AWARD', '시상', '/commission/award', 'commission/Award', 120),
('LIFE_OP_SUPPORT', '생보운영지원수수료', '/commission/life-support', 'commission/LifeSupport', 130),
('BRANCH_MGR_ALLOWANCE', '지점장수당', '/commission/branch-allowance', 'commission/BranchAllowance', 140)
) 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);
-- ============================================================
-- 2. 7개 메뉴에 권한 항목 등록 (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 IN (
'INCOME_COMM','REFERRAL_COMM','SETTLING_SUPPORT',
'RECOGNIZED_PERF','AWARD','LIFE_OP_SUPPORT','BRANCH_MGR_ALLOWANCE'
)
ON CONFLICT (menu_id, perm_code) DO NOTHING;
-- ============================================================
-- 3. 역할별 권한 부여
-- ============================================================
-- SUPER_ADMIN / ADMIN — 7개 메뉴 전체 권한
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 IN (
'INCOME_COMM','REFERRAL_COMM','SETTLING_SUPPORT',
'RECOGNIZED_PERF','AWARD','LIFE_OP_SUPPORT','BRANCH_MGR_ALLOWANCE'
)
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
-- MANAGER — READ/CREATE/UPDATE/EXPORT (DELETE 제외)
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 IN (
'INCOME_COMM','REFERRAL_COMM','SETTLING_SUPPORT',
'RECOGNIZED_PERF','AWARD','LIFE_OP_SUPPORT','BRANCH_MGR_ALLOWANCE'
)
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
-- AGENT — INCOME_COMM 제외하고 READ 만
-- (INCOME_COMM은 보험사-GA 수입 측 데이터로 설계사 공개 불가)
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 IN (
'REFERRAL_COMM','SETTLING_SUPPORT',
'RECOGNIZED_PERF','AWARD','LIFE_OP_SUPPORT','BRANCH_MGR_ALLOWANCE'
)
ON CONFLICT (role_id, menu_id, perm_code) DO NOTHING;
@@ -0,0 +1,35 @@
-- V96: incar 갭 수수료 도메인 공통코드 5종 (V55 패턴 동일)
-- INCOME_STATUS / REFERRAL_TYPE / CALC_TYPE / SUPPORT_STATUS / AWARD_TYPE
-- ===== 그룹 =====
INSERT INTO common_code_group (group_code, group_name, description, is_system, sort_order) VALUES
('INCOME_STATUS', '수입수수료상태', 'CALCULATED/CONFIRMED/CANCELLED', 'Y', 390),
('REFERRAL_TYPE', '소개이관유형', 'REFERRAL=소개/TRANSFER=이관', 'Y', 400),
('CALC_TYPE', '계산방식', 'RATE=정률/FIXED=정액', 'Y', 410),
('SUPPORT_STATUS', '지원금상태', 'CALCULATED/PAID/CLAWED_BACK/CANCELLED', 'Y', 420),
('AWARD_TYPE', '시상유형', 'NEW_RECRUIT/CORPORATE/RETENTION/PERFORMANCE', 'Y', 430)
ON CONFLICT (group_code) DO NOTHING;
-- ===== 상세 =====
INSERT INTO common_code (group_code, code, code_name, sort_order) VALUES
-- INCOME_STATUS
('INCOME_STATUS', 'CALCULATED', '계산완료', 10),
('INCOME_STATUS', 'CONFIRMED', '확정', 20),
('INCOME_STATUS', 'CANCELLED', '취소', 30),
-- REFERRAL_TYPE
('REFERRAL_TYPE', 'REFERRAL', '소개', 10),
('REFERRAL_TYPE', 'TRANSFER', '이관', 20),
-- CALC_TYPE
('CALC_TYPE', 'RATE', '정률', 10),
('CALC_TYPE', 'FIXED', '정액', 20),
-- SUPPORT_STATUS
('SUPPORT_STATUS', 'CALCULATED', '계산완료', 10),
('SUPPORT_STATUS', 'PAID', '지급완료', 20),
('SUPPORT_STATUS', 'CLAWED_BACK','환수완료', 30),
('SUPPORT_STATUS', 'CANCELLED', '취소', 40),
-- AWARD_TYPE
('AWARD_TYPE', 'NEW_RECRUIT', '신인도입', 10),
('AWARD_TYPE', 'CORPORATE', '법인', 20),
('AWARD_TYPE', 'RETENTION', '유지율', 30),
('AWARD_TYPE', 'PERFORMANCE', '실적', 40)
ON CONFLICT (group_code, code) DO NOTHING;
@@ -0,0 +1,27 @@
-- V97: 단계마감 (settlement_step_close)
-- 월 정산을 단계별로 마감(수신→계산→검증→지급). 기존 PERIOD_CLOSE 보강.
CREATE TABLE settlement_step_close (
step_close_id BIGSERIAL PRIMARY KEY,
settle_month CHAR(6) NOT NULL,
step_code VARCHAR(20) NOT NULL,
step_seq INT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'OPEN',
closed_at TIMESTAMP,
closed_by BIGINT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (settle_month, step_code),
CHECK (step_code IN ('RECEIVE','CALC','VERIFY','PAY')),
CHECK (status IN ('OPEN','CLOSED'))
);
COMMENT ON TABLE settlement_step_close IS '단계마감 — 월 정산 단계별 마감 상태 관리';
COMMENT ON COLUMN settlement_step_close.settle_month IS '정산 귀속월 (YYYYMM)';
COMMENT ON COLUMN settlement_step_close.step_code IS '단계코드 (RECEIVE=수신/CALC=계산/VERIFY=검증/PAY=지급)';
COMMENT ON COLUMN settlement_step_close.step_seq IS '단계 순서 1~4';
COMMENT ON COLUMN settlement_step_close.status IS '상태 (OPEN=열림/CLOSED=마감)';
COMMENT ON COLUMN settlement_step_close.closed_at IS '마감 처리 일시';
COMMENT ON COLUMN settlement_step_close.closed_by IS '마감 처리자 FK(users)';
CREATE INDEX idx_ssc_month_status ON settlement_step_close(settle_month, status);
CREATE INDEX idx_ssc_step_seq ON settlement_step_close(step_seq);
@@ -0,0 +1,29 @@
-- V98: 본사정산 (hq_settle_item)
-- GA 본사 자체의 수입/비용 정산 항목(설계사 지급과 별개의 GA 손익).
-- 본사손익 = SUM(INCOME amount) - SUM(EXPENSE amount)
CREATE TABLE hq_settle_item (
item_id BIGSERIAL PRIMARY KEY,
settle_month CHAR(6) NOT NULL,
item_type VARCHAR(20) NOT NULL,
item_name VARCHAR(100) NOT NULL,
amount NUMERIC(15,2) NOT NULL,
memo VARCHAR(300),
status VARCHAR(20) NOT NULL DEFAULT 'REGISTERED',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
created_by BIGINT,
updated_at TIMESTAMP,
CHECK (item_type IN ('INCOME','EXPENSE')),
CHECK (status IN ('REGISTERED','CONFIRMED'))
);
COMMENT ON TABLE hq_settle_item IS '본사정산 항목 — GA 자체 수입/비용 정산';
COMMENT ON COLUMN hq_settle_item.settle_month IS '정산 귀속월 (YYYYMM)';
COMMENT ON COLUMN hq_settle_item.item_type IS '항목유형 (INCOME=수입/EXPENSE=비용)';
COMMENT ON COLUMN hq_settle_item.item_name IS '항목명';
COMMENT ON COLUMN hq_settle_item.amount IS '금액 (원, HALF_UP)';
COMMENT ON COLUMN hq_settle_item.memo IS '비고';
COMMENT ON COLUMN hq_settle_item.status IS '상태 (REGISTERED=등록/CONFIRMED=확정)';
CREATE INDEX idx_hqi_month_type ON hq_settle_item(settle_month, item_type);
CREATE INDEX idx_hqi_month_status ON hq_settle_item(settle_month, status);
@@ -0,0 +1,53 @@
-- V99: 등급평가 (grade_eval_rule + agent_grade_evaluation)
-- 설계사 실적 점수로 등급 산정.
-- 공식: evaluated_grade = score 이상인 최대 min_score 룰의 grade (Service 결정)
-- ============================================================
-- 1. grade_eval_rule — 점수구간별 등급 룰 마스터
-- ============================================================
CREATE TABLE grade_eval_rule (
rule_id BIGSERIAL PRIMARY KEY,
grade_id INT NOT NULL REFERENCES grade(grade_id),
min_score NUMERIC(7,2) NOT NULL,
effective_from DATE NOT NULL,
effective_to DATE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE grade_eval_rule IS '등급평가 룰 — 실적점수 구간별 등급 매핑';
COMMENT ON COLUMN grade_eval_rule.grade_id IS '부여 등급 FK(grade)';
COMMENT ON COLUMN grade_eval_rule.min_score IS '최소 실적점수 (이 점수 이상이면 해당 등급)';
COMMENT ON COLUMN grade_eval_rule.effective_from IS '룰 적용 시작일';
COMMENT ON COLUMN grade_eval_rule.effective_to IS '룰 적용 종료일 (NULL=현재 유효)';
CREATE INDEX idx_ger_grade_from ON grade_eval_rule(grade_id, effective_from);
CREATE INDEX idx_ger_score ON grade_eval_rule(min_score);
-- ============================================================
-- 2. agent_grade_evaluation — 설계사 등급평가 원장
-- ============================================================
CREATE TABLE agent_grade_evaluation (
eval_id BIGSERIAL PRIMARY KEY,
agent_id INT NOT NULL REFERENCES agent(agent_id),
eval_period CHAR(6) NOT NULL,
performance_score NUMERIC(7,2) NOT NULL,
prev_grade_id INT REFERENCES grade(grade_id),
evaluated_grade_id INT REFERENCES grade(grade_id),
status VARCHAR(20) NOT NULL DEFAULT 'EVALUATED',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (agent_id, eval_period),
CHECK (status IN ('EVALUATED','APPLIED'))
);
COMMENT ON TABLE agent_grade_evaluation IS '설계사 등급평가 원장 — 월별 실적점수 기반 등급 산정 결과';
COMMENT ON COLUMN agent_grade_evaluation.agent_id IS '대상 설계사 FK';
COMMENT ON COLUMN agent_grade_evaluation.eval_period IS '평가 기준월 (YYYYMM)';
COMMENT ON COLUMN agent_grade_evaluation.performance_score IS '실적 점수';
COMMENT ON COLUMN agent_grade_evaluation.prev_grade_id IS '기존(직전) 등급 FK';
COMMENT ON COLUMN agent_grade_evaluation.evaluated_grade_id IS '산정 등급 FK';
COMMENT ON COLUMN agent_grade_evaluation.status IS '상태 (EVALUATED=평가완료/APPLIED=반영완료)';
CREATE INDEX idx_age_agent_period ON agent_grade_evaluation(agent_id, eval_period);
CREATE INDEX idx_age_status ON agent_grade_evaluation(status, eval_period);
@@ -0,0 +1,30 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.AgentGradeEvaluationResp;
import com.ga.core.vo.commission.AgentGradeEvaluationSearchParam;
import com.ga.core.vo.commission.AgentGradeEvaluationVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* agent_grade_evaluation Mapper.
* 설계사 등급평가 원장: 월별 실적점수 기반 등급 산정 결과.
*/
@Mapper
public interface AgentGradeEvaluationMapper {
AgentGradeEvaluationResp selectById(@Param("evalId") Long evalId);
List<AgentGradeEvaluationResp> selectList(AgentGradeEvaluationSearchParam param);
int insert(AgentGradeEvaluationVO vo);
/**
* 등급 반영 상태 변경 (EVALUATED APPLIED).
*/
int updateStatus(@Param("evalId") Long evalId, @Param("status") String status);
int delete(@Param("evalId") Long evalId);
}
@@ -0,0 +1,27 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.AgentGuaranteeInsuranceResp;
import com.ga.core.vo.commission.AgentGuaranteeInsuranceSearchParam;
import com.ga.core.vo.commission.AgentGuaranteeInsuranceVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* agent_guarantee_insurance Mapper.
* 보증보험: 설계사 신원보증보험 가입현황 관리.
*/
@Mapper
public interface AgentGuaranteeInsuranceMapper {
AgentGuaranteeInsuranceResp selectById(@Param("guaranteeId") Long guaranteeId);
List<AgentGuaranteeInsuranceResp> selectList(AgentGuaranteeInsuranceSearchParam param);
int insert(AgentGuaranteeInsuranceVO vo);
int update(AgentGuaranteeInsuranceVO vo);
int delete(@Param("guaranteeId") Long guaranteeId);
}
@@ -0,0 +1,41 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.AgentReserveFundResp;
import com.ga.core.vo.commission.AgentReserveFundSearchParam;
import com.ga.core.vo.commission.AgentReserveFundVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* agent_reserve_fund Mapper.
* 적립금: 설계사별 월별 적립금 잔액 관리.
* balance = 전월잔액 + reserve_in - reserve_out (Service 계산).
*/
@Mapper
public interface AgentReserveFundMapper {
AgentReserveFundResp selectById(@Param("reserveId") Long reserveId);
List<AgentReserveFundResp> selectList(AgentReserveFundSearchParam param);
/**
* 설계사 + 정산월 단건 조회 중복 방지 해당 잔액 확인용.
*/
AgentReserveFundVO selectByAgentAndMonth(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth);
/**
* 설계사의 지정월 이전 최신(직전월) 레코드 1건 잔액 누적 계산용.
* settleMonth 미만인 데이터 settle_month DESC LIMIT 1.
*/
AgentReserveFundVO selectLatestBefore(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth);
int insert(AgentReserveFundVO vo);
int update(AgentReserveFundVO vo);
int delete(@Param("reserveId") Long reserveId);
}
@@ -0,0 +1,35 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.AwardLedgerResp;
import com.ga.core.vo.commission.AwardLedgerSearchParam;
import com.ga.core.vo.commission.AwardLedgerVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* award_ledger Mapper 시상 원장 (V92)
*/
@Mapper
public interface AwardLedgerMapper {
AwardLedgerResp selectById(@Param("ledgerId") Long ledgerId);
List<AwardLedgerResp> selectList(AwardLedgerSearchParam param);
/** 설계사 + 정산월 시상금 합계 */
BigDecimal sumAwardByAgentAndMonth(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth);
/** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
int insert(AwardLedgerVO vo);
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
int delete(@Param("ledgerId") Long ledgerId);
}
@@ -0,0 +1,34 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.AwardRuleResp;
import com.ga.core.vo.commission.AwardRuleSearchParam;
import com.ga.core.vo.commission.AwardRuleVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
/**
* award_rule Mapper 시상 마스터 (V92)
*/
@Mapper
public interface AwardRuleMapper {
AwardRuleResp selectById(@Param("ruleId") Long ruleId);
List<AwardRuleResp> selectList(AwardRuleSearchParam param);
/**
* 시상 유형 + 기준일 유효 1건 조회.
* 시상 계산 Service에서 호출.
*/
AwardRuleVO selectActiveRule(@Param("awardType") String awardType,
@Param("baseDate") LocalDate baseDate);
int insert(AwardRuleVO vo);
int update(AwardRuleVO vo);
int delete(@Param("ruleId") Long ruleId);
}
@@ -0,0 +1,35 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.BranchMgrAllowanceLedgerResp;
import com.ga.core.vo.commission.BranchMgrAllowanceLedgerSearchParam;
import com.ga.core.vo.commission.BranchMgrAllowanceLedgerVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* branch_mgr_allowance_ledger Mapper 지점장수당 원장 (V94)
*/
@Mapper
public interface BranchMgrAllowanceLedgerMapper {
BranchMgrAllowanceLedgerResp selectById(@Param("ledgerId") Long ledgerId);
List<BranchMgrAllowanceLedgerResp> selectList(BranchMgrAllowanceLedgerSearchParam param);
/** 지점장 + 정산월 수당 합계 */
BigDecimal sumAllowanceByManagerAndMonth(@Param("managerAgentId") Long managerAgentId,
@Param("settleMonth") String settleMonth);
/** 정산월 기준 지점장별 순수당 합계(allowance_amount - COALESCE(clawback_amount,0)) — settle_master 집계 배치 용도 */
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
int insert(BranchMgrAllowanceLedgerVO vo);
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
int delete(@Param("ledgerId") Long ledgerId);
}
@@ -0,0 +1,34 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.BranchMgrAllowanceRuleResp;
import com.ga.core.vo.commission.BranchMgrAllowanceRuleSearchParam;
import com.ga.core.vo.commission.BranchMgrAllowanceRuleVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
/**
* branch_mgr_allowance_rule Mapper 지점장수당 (V94)
*/
@Mapper
public interface BranchMgrAllowanceRuleMapper {
BranchMgrAllowanceRuleResp selectById(@Param("ruleId") Long ruleId);
List<BranchMgrAllowanceRuleResp> selectList(BranchMgrAllowanceRuleSearchParam param);
/**
* 직책코드 + 기준일 유효 1건 조회.
* 지점장수당 계산 Service에서 호출.
*/
BranchMgrAllowanceRuleVO selectActiveRule(@Param("positionCode") String positionCode,
@Param("baseDate") LocalDate baseDate);
int insert(BranchMgrAllowanceRuleVO vo);
int update(BranchMgrAllowanceRuleVO vo);
int delete(@Param("ruleId") Long ruleId);
}
@@ -8,6 +8,7 @@ import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* collection_commission_ledger Mapper. * collection_commission_ledger Mapper.
@@ -23,6 +24,9 @@ public interface CollectionCommissionLedgerMapper {
BigDecimal sumByAgentAndMonth(@Param("agentId") Long agentId, BigDecimal sumByAgentAndMonth(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth); @Param("settleMonth") String settleMonth);
/** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
int insert(CollectionCommissionLedgerVO vo); int insert(CollectionCommissionLedgerVO vo);
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status); int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
@@ -0,0 +1,25 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.CommissionSimulationResp;
import com.ga.core.vo.commission.CommissionSimulationSearchParam;
import com.ga.core.vo.commission.CommissionSimulationVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* commission_simulation Mapper.
* 수수료 시뮬레이터: 시나리오별 산출 결과 저장/조회.
*/
@Mapper
public interface CommissionSimulationMapper {
CommissionSimulationResp selectById(@Param("simId") Long simId);
List<CommissionSimulationResp> selectList(CommissionSimulationSearchParam param);
int insert(CommissionSimulationVO vo);
int delete(@Param("simId") Long simId);
}
@@ -0,0 +1,36 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.GradeEvalRuleResp;
import com.ga.core.vo.commission.GradeEvalRuleSearchParam;
import com.ga.core.vo.commission.GradeEvalRuleVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
/**
* grade_eval_rule Mapper.
* 등급평가 : 점수 구간별 등급 매핑 마스터.
*/
@Mapper
public interface GradeEvalRuleMapper {
GradeEvalRuleResp selectById(@Param("ruleId") Long ruleId);
List<GradeEvalRuleResp> selectList(GradeEvalRuleSearchParam param);
/**
* 점수 이상인 최대 min_score 1건 조회 등급 산정 Service 용도.
* 기준일(baseDate) 기준 유효한 score 이상이고 min_score 최대인 1건 반환.
*/
GradeEvalRuleVO selectActiveRuleByScore(@Param("score") BigDecimal score,
@Param("baseDate") LocalDate baseDate);
int insert(GradeEvalRuleVO vo);
int update(GradeEvalRuleVO vo);
int delete(@Param("ruleId") Long ruleId);
}
@@ -0,0 +1,35 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.HqSettleItemResp;
import com.ga.core.vo.commission.HqSettleItemSearchParam;
import com.ga.core.vo.commission.HqSettleItemVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
/**
* hq_settle_item Mapper.
* 본사정산: GA 자체 수입/비용 항목. 손익 = Σ(INCOME) Σ(EXPENSE).
*/
@Mapper
public interface HqSettleItemMapper {
HqSettleItemResp selectById(@Param("itemId") Long itemId);
List<HqSettleItemResp> selectList(HqSettleItemSearchParam param);
/**
* 정산월 + 항목유형별 금액 합계 본사손익 집계용.
* itemType = INCOME or EXPENSE
*/
BigDecimal sumByMonthAndType(@Param("settleMonth") String settleMonth,
@Param("itemType") String itemType);
int insert(HqSettleItemVO vo);
int update(HqSettleItemVO vo);
int delete(@Param("itemId") Long itemId);
}
@@ -0,0 +1,31 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.IncomeCommissionLedgerResp;
import com.ga.core.vo.commission.IncomeCommissionLedgerSearchParam;
import com.ga.core.vo.commission.IncomeCommissionLedgerVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
/**
* income_commission_ledger Mapper 수입수수료 원장 (V88)
*/
@Mapper
public interface IncomeCommissionLedgerMapper {
IncomeCommissionLedgerResp selectById(@Param("incomeId") Long incomeId);
List<IncomeCommissionLedgerResp> selectList(IncomeCommissionLedgerSearchParam param);
/** 보험사 + 정산월 수입수수료 합계 — margin_summary recalc 배치 용도 */
BigDecimal sumByCarrierAndMonth(@Param("carrierId") Long carrierId,
@Param("settleMonth") String settleMonth);
int insert(IncomeCommissionLedgerVO vo);
int updateStatus(@Param("incomeId") Long incomeId, @Param("status") String status);
int delete(@Param("incomeId") Long incomeId);
}
@@ -0,0 +1,38 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.JointContractResp;
import com.ga.core.vo.commission.JointContractSearchParam;
import com.ga.core.vo.commission.JointContractVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
/**
* joint_contract Mapper.
* 공동계약: 복수 설계사의 계약별 지분율 관리.
*/
@Mapper
public interface JointContractMapper {
JointContractResp selectById(@Param("jointId") Long jointId);
List<JointContractResp> selectList(JointContractSearchParam param);
/**
* 계약별 공동설계사 목록 조회 지분율 확인용.
*/
List<JointContractResp> selectByContract(@Param("contractId") Long contractId);
/**
* 계약별 지분율 합산 =1 검증(Service 가드) 용도.
*/
BigDecimal sumShareByContract(@Param("contractId") Long contractId);
int insert(JointContractVO vo);
int update(JointContractVO vo);
int delete(@Param("jointId") Long jointId);
}
@@ -0,0 +1,35 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.LifeSupportLedgerResp;
import com.ga.core.vo.commission.LifeSupportLedgerSearchParam;
import com.ga.core.vo.commission.LifeSupportLedgerVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* life_support_ledger Mapper 생보운영지원수수료 원장 (V93)
*/
@Mapper
public interface LifeSupportLedgerMapper {
LifeSupportLedgerResp selectById(@Param("ledgerId") Long ledgerId);
List<LifeSupportLedgerResp> selectList(LifeSupportLedgerSearchParam param);
/** 설계사 + 정산월 실지급액 합계 */
BigDecimal sumNetByAgentAndMonth(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth);
/** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
int insert(LifeSupportLedgerVO vo);
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
int delete(@Param("ledgerId") Long ledgerId);
}
@@ -0,0 +1,34 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.LifeSupportRuleResp;
import com.ga.core.vo.commission.LifeSupportRuleSearchParam;
import com.ga.core.vo.commission.LifeSupportRuleVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
/**
* life_support_rule Mapper 생보운영지원수수료 (V93)
*/
@Mapper
public interface LifeSupportRuleMapper {
LifeSupportRuleResp selectById(@Param("ruleId") Long ruleId);
List<LifeSupportRuleResp> selectList(LifeSupportRuleSearchParam param);
/**
* 설계사 등급 + 기준일 유효 1건 조회.
* 생보운영지원수수료 계산 Service에서 호출.
*/
LifeSupportRuleVO selectActiveRule(@Param("agentGrade") String agentGrade,
@Param("baseDate") LocalDate baseDate);
int insert(LifeSupportRuleVO vo);
int update(LifeSupportRuleVO vo);
int delete(@Param("ruleId") Long ruleId);
}
@@ -0,0 +1,41 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.MarginSummaryResp;
import com.ga.core.vo.commission.MarginSummarySearchParam;
import com.ga.core.vo.commission.MarginSummaryVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
/**
* margin_summary Mapper 수지차 집계 (V88)
*/
@Mapper
public interface MarginSummaryMapper {
MarginSummaryResp selectById(@Param("marginId") Long marginId);
List<MarginSummaryResp> selectList(MarginSummarySearchParam param);
/**
* 정산월 + 설계사(NULL=전체) 단위 수지차 1건 조회.
* recalc UPSERT 존재 여부 확인용.
*/
MarginSummaryVO selectByMonthAndAgent(@Param("settleMonth") String settleMonth,
@Param("agentId") Long agentId);
/** 정산월 전체 수입수수료 합 (income_commission_ledger, CANCELLED 제외). */
BigDecimal sumIncomeByMonth(@Param("settleMonth") String settleMonth);
/** 정산월 지급 합 (settle_master.net_amount). agentId NULL=전체. */
BigDecimal sumPayoutByMonth(@Param("settleMonth") String settleMonth,
@Param("agentId") Long agentId);
int insert(MarginSummaryVO vo);
int update(MarginSummaryVO vo);
int delete(@Param("marginId") Long marginId);
}
@@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* persistency_bonus Mapper. * persistency_bonus Mapper.
@@ -30,4 +31,7 @@ public interface PersistencyBonusMapper {
int updateStatus(@Param("bonusId") Long bonusId, @Param("status") String status); int updateStatus(@Param("bonusId") Long bonusId, @Param("status") String status);
int delete(@Param("bonusId") Long bonusId); int delete(@Param("bonusId") Long bonusId);
/** 정산월 기준 설계사별 유지보너스 합계 — settle_master 집계 배치 용도 */
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
} }
@@ -0,0 +1,34 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.ProductConversionRateResp;
import com.ga.core.vo.commission.ProductConversionRateSearchParam;
import com.ga.core.vo.commission.ProductConversionRateVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
/**
* product_conversion_rate Mapper 상품별 환산율 마스터 (V91)
*/
@Mapper
public interface ProductConversionRateMapper {
ProductConversionRateResp selectById(@Param("rateId") Long rateId);
List<ProductConversionRateResp> selectList(ProductConversionRateSearchParam param);
/**
* 상품 + 기준일 유효 환산율 1건 조회.
* 인정실적 계산 Service에서 호출.
*/
ProductConversionRateVO selectActiveRate(@Param("productId") Long productId,
@Param("baseDate") LocalDate baseDate);
int insert(ProductConversionRateVO vo);
int update(ProductConversionRateVO vo);
int delete(@Param("rateId") Long rateId);
}
@@ -0,0 +1,31 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.RecognizedPerformanceLedgerResp;
import com.ga.core.vo.commission.RecognizedPerformanceLedgerSearchParam;
import com.ga.core.vo.commission.RecognizedPerformanceLedgerVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
/**
* recognized_performance_ledger Mapper 인정실적 원장 (V91)
*/
@Mapper
public interface RecognizedPerformanceLedgerMapper {
RecognizedPerformanceLedgerResp selectById(@Param("ledgerId") Long ledgerId);
List<RecognizedPerformanceLedgerResp> selectList(RecognizedPerformanceLedgerSearchParam param);
/** 설계사 + 정산월 인정실적 합계 — 시상 기준 실적 산출 용도 */
BigDecimal sumRecognizedByAgentAndMonth(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth);
int insert(RecognizedPerformanceLedgerVO vo);
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
int delete(@Param("ledgerId") Long ledgerId);
}
@@ -0,0 +1,35 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.ReferralCommissionLedgerResp;
import com.ga.core.vo.commission.ReferralCommissionLedgerSearchParam;
import com.ga.core.vo.commission.ReferralCommissionLedgerVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* referral_commission_ledger Mapper 소개·이관수수료 원장 (V89)
*/
@Mapper
public interface ReferralCommissionLedgerMapper {
ReferralCommissionLedgerResp selectById(@Param("ledgerId") Long ledgerId);
List<ReferralCommissionLedgerResp> selectList(ReferralCommissionLedgerSearchParam param);
/** 설계사 + 정산월 소개·이관수수료 합계 — commission_master 집계 배치 용도 */
BigDecimal sumByAgentAndMonth(@Param("referrerAgentId") Long referrerAgentId,
@Param("settleMonth") String settleMonth);
/** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
int insert(ReferralCommissionLedgerVO vo);
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
int delete(@Param("ledgerId") Long ledgerId);
}
@@ -0,0 +1,34 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.ReferralCommissionRuleResp;
import com.ga.core.vo.commission.ReferralCommissionRuleSearchParam;
import com.ga.core.vo.commission.ReferralCommissionRuleVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
/**
* referral_commission_rule Mapper 소개·이관수수료 (V89)
*/
@Mapper
public interface ReferralCommissionRuleMapper {
ReferralCommissionRuleResp selectById(@Param("ruleId") Long ruleId);
List<ReferralCommissionRuleResp> selectList(ReferralCommissionRuleSearchParam param);
/**
* 유형 + 기준일 기준 유효 1건 조회.
* 수수료 계산 Service에서 호출.
*/
ReferralCommissionRuleVO selectActiveRule(@Param("referralType") String referralType,
@Param("baseDate") LocalDate baseDate);
int insert(ReferralCommissionRuleVO vo);
int update(ReferralCommissionRuleVO vo);
int delete(@Param("ruleId") Long ruleId);
}
@@ -8,6 +8,7 @@ import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map;
@Mapper @Mapper
public interface ReinstatementCommissionLedgerMapper { public interface ReinstatementCommissionLedgerMapper {
@@ -15,6 +16,9 @@ public interface ReinstatementCommissionLedgerMapper {
List<ReinstatementCommissionLedgerResp> selectList(ReinstatementCommissionLedgerSearchParam param); List<ReinstatementCommissionLedgerResp> selectList(ReinstatementCommissionLedgerSearchParam param);
BigDecimal sumByAgentAndMonth(@Param("agentId") Long agentId, BigDecimal sumByAgentAndMonth(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth); @Param("settleMonth") String settleMonth);
/** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
int insert(ReinstatementCommissionLedgerVO vo); int insert(ReinstatementCommissionLedgerVO vo);
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status); int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
int delete(@Param("ledgerId") Long ledgerId); int delete(@Param("ledgerId") Long ledgerId);
@@ -8,6 +8,7 @@ import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Map;
@Mapper @Mapper
public interface RenewalCommissionLedgerMapper { public interface RenewalCommissionLedgerMapper {
@@ -15,6 +16,9 @@ public interface RenewalCommissionLedgerMapper {
List<RenewalCommissionLedgerResp> selectList(RenewalCommissionLedgerSearchParam param); List<RenewalCommissionLedgerResp> selectList(RenewalCommissionLedgerSearchParam param);
BigDecimal sumByAgentAndMonth(@Param("agentId") Long agentId, BigDecimal sumByAgentAndMonth(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth); @Param("settleMonth") String settleMonth);
/** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
int insert(RenewalCommissionLedgerVO vo); int insert(RenewalCommissionLedgerVO vo);
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status); int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
int delete(@Param("ledgerId") Long ledgerId); int delete(@Param("ledgerId") Long ledgerId);
@@ -0,0 +1,35 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.SettlingSupportLedgerResp;
import com.ga.core.vo.commission.SettlingSupportLedgerSearchParam;
import com.ga.core.vo.commission.SettlingSupportLedgerVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* settling_support_ledger Mapper 정착지원금 원장 (V90)
*/
@Mapper
public interface SettlingSupportLedgerMapper {
SettlingSupportLedgerResp selectById(@Param("ledgerId") Long ledgerId);
List<SettlingSupportLedgerResp> selectList(SettlingSupportLedgerSearchParam param);
/** 설계사 + 정산월 실지급액 합계 */
BigDecimal sumNetByAgentAndMonth(@Param("agentId") Long agentId,
@Param("settleMonth") String settleMonth);
/** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
int insert(SettlingSupportLedgerVO vo);
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
int delete(@Param("ledgerId") Long ledgerId);
}
@@ -0,0 +1,34 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.SettlingSupportRuleResp;
import com.ga.core.vo.commission.SettlingSupportRuleSearchParam;
import com.ga.core.vo.commission.SettlingSupportRuleVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDate;
import java.util.List;
/**
* settling_support_rule Mapper 정착지원금 (V90)
*/
@Mapper
public interface SettlingSupportRuleMapper {
SettlingSupportRuleResp selectById(@Param("ruleId") Long ruleId);
List<SettlingSupportRuleResp> selectList(SettlingSupportRuleSearchParam param);
/**
* 설계사 등급 + 기준일 유효 1건 조회.
* 정착지원금 계산 Service에서 호출.
*/
SettlingSupportRuleVO selectActiveRule(@Param("agentGrade") String agentGrade,
@Param("baseDate") LocalDate baseDate);
int insert(SettlingSupportRuleVO vo);
int update(SettlingSupportRuleVO vo);
int delete(@Param("ruleId") Long ruleId);
}
@@ -0,0 +1,36 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.StepCloseResp;
import com.ga.core.vo.commission.StepCloseSearchParam;
import com.ga.core.vo.commission.StepCloseVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* settlement_step_close Mapper.
* 단계마감: 월별 정산 단계(수신계산검증지급) OPEN/CLOSED 상태 관리.
*/
@Mapper
public interface StepCloseMapper {
StepCloseResp selectById(@Param("stepCloseId") Long stepCloseId);
List<StepCloseResp> selectList(StepCloseSearchParam param);
/** 정산월 전체 단계 목록 (step_seq 순) */
List<StepCloseResp> selectBySettleMonth(@Param("settleMonth") String settleMonth);
int insert(StepCloseVO vo);
/**
* 단계 상태 변경 (close: CLOSED, reopen: OPEN).
* closedAt/closedBy SQL에서 NOW()/userId 처리.
*/
int updateStatus(@Param("stepCloseId") Long stepCloseId,
@Param("status") String status,
@Param("closedBy") Long closedBy);
int delete(@Param("stepCloseId") Long stepCloseId);
}
@@ -0,0 +1,26 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* agent_grade_evaluation 조회 응답 설계사명/등급명 조인 포함.
*/
@Data
public class AgentGradeEvaluationResp {
private Long evalId;
private Long agentId;
private String agentName;
private String orgName;
private String evalPeriod;
private BigDecimal performanceScore;
private Long prevGradeId;
private String prevGradeName;
private Long evaluatedGradeId;
private String evaluatedGradeName;
private String status;
private String statusName;
private LocalDateTime createdAt;
}
@@ -0,0 +1,29 @@
package com.ga.core.vo.commission;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
import java.math.BigDecimal;
/**
* agent_grade_evaluation 저장 요청.
* evaluatedGradeId/status Service(등급산정 로직) 결정.
*/
@Data
public class AgentGradeEvaluationSaveReq {
@NotNull
private Long agentId;
@NotBlank
private String evalPeriod;
@NotNull
@Positive
private BigDecimal performanceScore;
/** 기존 등급 — 선택값 */
private Long prevGradeId;
}
@@ -0,0 +1,16 @@
package com.ga.core.vo.commission;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* agent_grade_evaluation 검색 조건.
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class AgentGradeEvaluationSearchParam extends SearchParam {
private Long agentId;
private String agentName;
private String evalPeriod;
}
@@ -0,0 +1,29 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* agent_grade_evaluation 설계사 등급평가 원장 (V99)
* 월별 실적점수 기반 등급 산정 결과.
* 공식: evaluated_grade = score 이상인 최대 min_score 룰의 grade (Service 결정).
*/
@Data
public class AgentGradeEvaluationVO {
private Long evalId;
/** 대상 설계사 FK */
private Long agentId;
/** 평가 기준월 (YYYYMM) */
private String evalPeriod;
/** 실적 점수 */
private BigDecimal performanceScore;
/** 기존(직전) 등급 FK */
private Long prevGradeId;
/** 산정 등급 FK */
private Long evaluatedGradeId;
/** EVALUATED=평가완료/APPLIED=반영완료 */
private String status;
private LocalDateTime createdAt;
}
@@ -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;
/**
* agent_guarantee_insurance 조회 응답 설계사명/보험사명 조인 포함.
*/
@Data
public class AgentGuaranteeInsuranceResp {
private Long guaranteeId;
private Long agentId;
private String agentName;
private String orgName;
private String policyNo;
private Long carrierId;
private String carrierName;
private BigDecimal guaranteeAmount;
private LocalDate startDate;
private LocalDate endDate;
private String status;
private String statusName;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,36 @@
package com.ga.core.vo.commission;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* agent_guarantee_insurance 저장 요청.
* status/createdAt/updatedAt 서버 결정.
*/
@Data
public class AgentGuaranteeInsuranceSaveReq {
@NotNull
private Long agentId;
@NotBlank
private String policyNo;
/** 보증보험사 — 선택 */
private Long carrierId;
@NotNull
@Positive
private BigDecimal guaranteeAmount;
@NotNull
private LocalDate startDate;
@NotNull
private LocalDate endDate;
}
@@ -0,0 +1,16 @@
package com.ga.core.vo.commission;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* agent_guarantee_insurance 검색 조건.
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class AgentGuaranteeInsuranceSearchParam extends SearchParam {
private Long agentId;
private String agentName;
private Long carrierId;
}
@@ -0,0 +1,29 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* agent_guarantee_insurance 보증보험 (V101)
* 설계사 신원보증보험 가입현황 추적(관리 목적, 공식 없음).
*/
@Data
public class AgentGuaranteeInsuranceVO {
private Long guaranteeId;
/** 설계사 FK */
private Long agentId;
/** 증권번호 */
private String policyNo;
/** 보증보험사 FK (insurance_company.company_id) */
private Long carrierId;
private BigDecimal guaranteeAmount;
private LocalDate startDate;
private LocalDate endDate;
/** ACTIVE=유효/EXPIRED=만료/CANCELLED=해지 */
private String status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,22 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* agent_reserve_fund 조회 응답 설계사명 조인 포함.
*/
@Data
public class AgentReserveFundResp {
private Long reserveId;
private Long agentId;
private String agentName;
private String orgName;
private String settleMonth;
private BigDecimal reserveIn;
private BigDecimal reserveOut;
private BigDecimal balance;
private LocalDateTime createdAt;
}

Some files were not shown because too many files have changed in this diff Show More