diff --git a/DOMAIN_KNOWLEDGE.md b/DOMAIN_KNOWLEDGE.md index 9eb1d56..5b26c0c 100644 --- a/DOMAIN_KNOWLEDGE.md +++ b/DOMAIN_KNOWLEDGE.md @@ -91,6 +91,13 @@ | **유지보수당(지속수당)** | persistency bonus | 일정 유지율 달성 시 | 설계사 | 보너스 성격 | | **오버라이드** | override | 하위 설계사 실적 발생 시 | **상위 직책자** | 조직 트리 기반 수당 | | **시책·인센티브** | incentive | 캠페인/목표 달성 시 | 설계사 | 한시적 프로모션 | +| **수입수수료** | income | 보험사가 GA에 지급한 수수료 수입 원장 | GA 경영진/재무 | 수지차(=수입-지급) 집계용. 설계사 지급과 분리 관리 | +| **소개·이관수수료** | referral/transfer | 계약을 소개(REFERRAL)하거나 이관(TRANSFER)한 설계사에게 | 소개/이관 설계사 | 정률 또는 정액. 기준금액×율 또는 고정액 | +| **정착지원금** | settling support | 신입/경력 설계사 입사 후 약정 기간 | 신규 설계사 | 등급별 월 정액. 조기 해촉 시 잔여약정 비율로 환수 | +| **인정실적** | recognized performance | 계약 체결 시 (환산보험료 기준) | 설계사 | 환산보험료×인정율. 시상·등급 산정의 기초 지표 | +| **시상** | award | 목표 실적 달성 시 | 설계사 | 신인도입/법인/유지율/실적 시상. 정률 또는 정액 | +| **생보운영지원수수료** | life op support | 생보 계약 운영 실적 기준 | 설계사 | 실적×지급률. 환수 가능 | +| **지점장수당** | branch mgr allowance | 소속 조직 실적 발생 시 | 지점장 | 조직실적×수당율, cap(상한) 제한. 환수 가능 | > **모집 vs 유지의 귀속 차이 (중요):** 설계사가 해촉·이관되면, 그 계약의 **유지수수료는 후임 설계사**에게 넘어가지만(계약 승계), > **모집수수료는 원래 모집한 설계사**에게 그대로 남는다. (한국 고아계약 승계 표준) @@ -245,7 +252,21 @@ gross(총수수료) > 여기에 가지급금 등 **공제**가 있으면 net에서 추가로 빠집니다. > 비과세/면세 설계사는 부가세(+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) @@ -359,5 +380,5 @@ DRAFT(계산됨) → CONFIRMED(확정) → PAID(지급완료) --- -*기준: 한국 보험업감독규정 표준 가정값. 시스템 schema v87 (2026-05 기준). 표준값은 모두 데이터로 외부화되어 코드 변경 없이 조정 가능.* -*계산 공식 출처: `CommissionCalculator` / `TaxCalculator` / `CalcOverrideStep` 실제 코드.* +*기준: 한국 보험업감독규정 표준 가정값. 시스템 schema v96 (2026-05-31 기준, P7 incar 갭 반영). 표준값은 모두 데이터로 외부화되어 코드 변경 없이 조정 가능.* +*계산 공식 출처: `CommissionCalculator` / `TaxCalculator` / `CalcOverrideStep` + P7 7개 도메인 Service (V88~V96) 실제 코드.* diff --git a/HANDOFF.md b/HANDOFF.md index 8372d7a..21c8875 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -377,7 +377,7 @@ PWA 스캐폴드에서 클라이언트가 `agentId` 를 쿼리 파라미터로 P5 전체 + P6 PoC + **P6 후속(2026-05-27)** 완료. 잔여는 외부 SDK 차단 항목만. ### 운영 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 모두 라이브. 단위/통합 테스트: **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 등) | 외부 채널 결정 | +## 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 라이브 적용) 설계사 해촉/이관 시 보험계약의 **유지수수료(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` 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. **재기동이 필요한 경우**: - `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) diff --git a/docs/DOMAIN_GAP_P7.md b/docs/DOMAIN_GAP_P7.md new file mode 100644 index 0000000..a0cbae7 --- /dev/null +++ b/docs/DOMAIN_GAP_P7.md @@ -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 오류 → ``로 교체. diff --git a/docs/DOMAIN_GAP_P8.md b/docs/DOMAIN_GAP_P8.md new file mode 100644 index 0000000..d19b4d4 --- /dev/null +++ b/docs/DOMAIN_GAP_P8.md @@ -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_commission−withholding_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.) diff --git a/docs/신입교육_보험과수수료_완전기초.md b/docs/신입교육_보험과수수료_완전기초.md new file mode 100644 index 0000000..a003883 --- /dev/null +++ b/docs/신입교육_보험과수수료_완전기초.md @@ -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) 참고.* diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/AwardController.java b/ga-api/src/main/java/com/ga/api/controller/commission/AwardController.java new file mode 100644 index 0000000..9f9aeda --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/AwardController.java @@ -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> listRules( + @ModelAttribute AwardRuleSearchParam param) { + return ApiResponse.ok(service.listRules(param)); + } + + @GetMapping("/rules/{ruleId}") + @RequirePermission(menu = "AWARD", perm = "READ") + public ApiResponse 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 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 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 deleteRule(@PathVariable Long ruleId) { + service.deleteRule(ruleId); + return ApiResponse.ok(); + } + + // ── Ledger ──────────────────────────────────────────────────────────────── + + @GetMapping("/ledgers") + @RequirePermission(menu = "AWARD", perm = "READ") + public ApiResponse> listLedgers( + @ModelAttribute AwardLedgerSearchParam param) { + return ApiResponse.ok(service.listLedgers(param)); + } + + @GetMapping("/ledgers/{ledgerId}") + @RequirePermission(menu = "AWARD", perm = "READ") + public ApiResponse 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 createLedger(@Valid @RequestBody AwardLedgerSaveReq req) { + return ApiResponse.ok(service.createLedger(req)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/BranchAllowanceController.java b/ga-api/src/main/java/com/ga/api/controller/commission/BranchAllowanceController.java new file mode 100644 index 0000000..10ddf8f --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/BranchAllowanceController.java @@ -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> listRules( + @ModelAttribute BranchMgrAllowanceRuleSearchParam param) { + return ApiResponse.ok(service.listRules(param)); + } + + @GetMapping("/rules/{ruleId}") + @RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ") + public ApiResponse 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 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 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 deleteRule(@PathVariable Long ruleId) { + service.deleteRule(ruleId); + return ApiResponse.ok(); + } + + // ── Ledger ──────────────────────────────────────────────────────────────── + + @GetMapping("/ledgers") + @RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ") + public ApiResponse> listLedgers( + @ModelAttribute BranchMgrAllowanceLedgerSearchParam param) { + return ApiResponse.ok(service.listLedgers(param)); + } + + @GetMapping("/ledgers/{ledgerId}") + @RequirePermission(menu = "BRANCH_MGR_ALLOWANCE", perm = "READ") + public ApiResponse 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 createLedger(@Valid @RequestBody BranchMgrAllowanceLedgerSaveReq req) { + return ApiResponse.ok(service.createLedger(req)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/CommissionSimulationController.java b/ga-api/src/main/java/com/ga/api/controller/commission/CommissionSimulationController.java new file mode 100644 index 0000000..dd65ace --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/CommissionSimulationController.java @@ -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> list( + @ModelAttribute CommissionSimulationSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{simId}") + @RequirePermission(menu = "COMM_SIMULATOR", perm = "READ") + public ApiResponse 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 simulate(@Valid @RequestBody CommissionSimulationSaveReq req) { + return ApiResponse.ok(service.simulate(req)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/GradeEvaluationController.java b/ga-api/src/main/java/com/ga/api/controller/commission/GradeEvaluationController.java new file mode 100644 index 0000000..faa9558 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/GradeEvaluationController.java @@ -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> listRules(@ModelAttribute GradeEvalRuleSearchParam param) { + return ApiResponse.ok(service.listRules(param)); + } + + @GetMapping("/rules/{ruleId}") + @RequirePermission(menu = "GRADE_EVAL", perm = "READ") + public ApiResponse 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 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 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 deleteRule(@PathVariable Long ruleId) { + service.deleteRule(ruleId); + return ApiResponse.ok(); + } + + // ── Evaluation ──────────────────────────────────────────────────────────── + + @GetMapping + @RequirePermission(menu = "GRADE_EVAL", perm = "READ") + public ApiResponse> listEvals( + @ModelAttribute AgentGradeEvaluationSearchParam param) { + return ApiResponse.ok(service.listEvals(param)); + } + + @GetMapping("/{evalId}") + @RequirePermission(menu = "GRADE_EVAL", perm = "READ") + public ApiResponse 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 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 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 deleteEval(@PathVariable Long evalId) { + service.deleteEval(evalId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/GuaranteeInsuranceController.java b/ga-api/src/main/java/com/ga/api/controller/commission/GuaranteeInsuranceController.java new file mode 100644 index 0000000..9a7bd12 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/GuaranteeInsuranceController.java @@ -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> list( + @ModelAttribute AgentGuaranteeInsuranceSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{guaranteeId}") + @RequirePermission(menu = "GUARANTEE_INS", perm = "READ") + public ApiResponse 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 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 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 delete(@PathVariable Long guaranteeId) { + service.delete(guaranteeId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/HqSettleController.java b/ga-api/src/main/java/com/ga/api/controller/commission/HqSettleController.java new file mode 100644 index 0000000..90eaba6 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/HqSettleController.java @@ -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> list(@ModelAttribute HqSettleItemSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/summary") + @RequirePermission(menu = "HQ_SETTLE", perm = "READ") + public ApiResponse summary(@RequestParam String settleMonth) { + return ApiResponse.ok(service.summary(settleMonth)); + } + + @GetMapping("/{itemId}") + @RequirePermission(menu = "HQ_SETTLE", perm = "READ") + public ApiResponse 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 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 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 delete(@PathVariable Long itemId) { + service.delete(itemId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/IncomeCommissionController.java b/ga-api/src/main/java/com/ga/api/controller/commission/IncomeCommissionController.java new file mode 100644 index 0000000..9cbccad --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/IncomeCommissionController.java @@ -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> listLedgers( + @ModelAttribute IncomeCommissionLedgerSearchParam param) { + return ApiResponse.ok(service.listLedgers(param)); + } + + @GetMapping("/ledgers/{incomeId}") + @RequirePermission(menu = "INCOME_COMM", perm = "READ") + public ApiResponse 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 createLedger(@Valid @RequestBody IncomeCommissionLedgerSaveReq req) { + return ApiResponse.ok(service.createLedger(req)); + } + + // ── Margin Summary ─────────────────────────────────────────────────────── + + @GetMapping("/margins") + @RequirePermission(menu = "INCOME_COMM", perm = "READ") + public ApiResponse> listMargins( + @ModelAttribute MarginSummarySearchParam param) { + return ApiResponse.ok(service.listMargins(param)); + } + + @GetMapping("/margins/{marginId}") + @RequirePermission(menu = "INCOME_COMM", perm = "READ") + public ApiResponse 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 recalcMargin(@RequestParam String settleMonth, + @RequestParam(required = false) Long agentId) { + return ApiResponse.ok(service.recalcMargin(settleMonth, agentId)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/JointContractController.java b/ga-api/src/main/java/com/ga/api/controller/commission/JointContractController.java new file mode 100644 index 0000000..503bea1 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/JointContractController.java @@ -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> list(@ModelAttribute JointContractSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{jointId}") + @RequirePermission(menu = "JOINT_CONTRACT", perm = "READ") + public ApiResponse 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 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 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 delete(@PathVariable Long jointId) { + service.delete(jointId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/LifeSupportController.java b/ga-api/src/main/java/com/ga/api/controller/commission/LifeSupportController.java new file mode 100644 index 0000000..61a2da7 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/LifeSupportController.java @@ -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> listRules( + @ModelAttribute LifeSupportRuleSearchParam param) { + return ApiResponse.ok(service.listRules(param)); + } + + @GetMapping("/rules/{ruleId}") + @RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ") + public ApiResponse 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 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 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 deleteRule(@PathVariable Long ruleId) { + service.deleteRule(ruleId); + return ApiResponse.ok(); + } + + // ── Ledger ──────────────────────────────────────────────────────────────── + + @GetMapping("/ledgers") + @RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ") + public ApiResponse> listLedgers( + @ModelAttribute LifeSupportLedgerSearchParam param) { + return ApiResponse.ok(service.listLedgers(param)); + } + + @GetMapping("/ledgers/{ledgerId}") + @RequirePermission(menu = "LIFE_OP_SUPPORT", perm = "READ") + public ApiResponse 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 createLedger(@Valid @RequestBody LifeSupportLedgerSaveReq req) { + return ApiResponse.ok(service.createLedger(req)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/RecognizedPerformanceController.java b/ga-api/src/main/java/com/ga/api/controller/commission/RecognizedPerformanceController.java new file mode 100644 index 0000000..d9cebe7 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/RecognizedPerformanceController.java @@ -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> listRates( + @ModelAttribute ProductConversionRateSearchParam param) { + return ApiResponse.ok(service.listRates(param)); + } + + @GetMapping("/rates/{rateId}") + @RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ") + public ApiResponse 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 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 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 deleteRate(@PathVariable Long rateId) { + service.deleteRate(rateId); + return ApiResponse.ok(); + } + + // ── Ledger ──────────────────────────────────────────────────────────────── + + @GetMapping("/ledgers") + @RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ") + public ApiResponse> listLedgers( + @ModelAttribute RecognizedPerformanceLedgerSearchParam param) { + return ApiResponse.ok(service.listLedgers(param)); + } + + @GetMapping("/ledgers/{ledgerId}") + @RequirePermission(menu = "RECOGNIZED_PERF", perm = "READ") + public ApiResponse 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 createLedger(@Valid @RequestBody RecognizedPerformanceLedgerSaveReq req) { + return ApiResponse.ok(service.createLedger(req)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/ReferralCommissionController.java b/ga-api/src/main/java/com/ga/api/controller/commission/ReferralCommissionController.java new file mode 100644 index 0000000..3c71fce --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/ReferralCommissionController.java @@ -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> listRules( + @ModelAttribute ReferralCommissionRuleSearchParam param) { + return ApiResponse.ok(service.listRules(param)); + } + + @GetMapping("/rules/{ruleId}") + @RequirePermission(menu = "REFERRAL_COMM", perm = "READ") + public ApiResponse 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 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 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 deleteRule(@PathVariable Long ruleId) { + service.deleteRule(ruleId); + return ApiResponse.ok(); + } + + // ── Ledger ──────────────────────────────────────────────────────────────── + + @GetMapping("/ledgers") + @RequirePermission(menu = "REFERRAL_COMM", perm = "READ") + public ApiResponse> listLedgers( + @ModelAttribute ReferralCommissionLedgerSearchParam param) { + return ApiResponse.ok(service.listLedgers(param)); + } + + @GetMapping("/ledgers/{ledgerId}") + @RequirePermission(menu = "REFERRAL_COMM", perm = "READ") + public ApiResponse 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 createLedger(@Valid @RequestBody ReferralCommissionLedgerSaveReq req) { + return ApiResponse.ok(service.createLedger(req)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/ReserveFundController.java b/ga-api/src/main/java/com/ga/api/controller/commission/ReserveFundController.java new file mode 100644 index 0000000..ceb67b6 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/ReserveFundController.java @@ -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> list(@ModelAttribute AgentReserveFundSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/{reserveId}") + @RequirePermission(menu = "RESERVE_FUND", perm = "READ") + public ApiResponse 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 create(@Valid @RequestBody AgentReserveFundSaveReq req) { + return ApiResponse.ok(service.create(req)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/SettlingSupportController.java b/ga-api/src/main/java/com/ga/api/controller/commission/SettlingSupportController.java new file mode 100644 index 0000000..dea0a89 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/SettlingSupportController.java @@ -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> listRules( + @ModelAttribute SettlingSupportRuleSearchParam param) { + return ApiResponse.ok(service.listRules(param)); + } + + @GetMapping("/rules/{ruleId}") + @RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ") + public ApiResponse 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 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 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 deleteRule(@PathVariable Long ruleId) { + service.deleteRule(ruleId); + return ApiResponse.ok(); + } + + // ── Ledger ──────────────────────────────────────────────────────────────── + + @GetMapping("/ledgers") + @RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ") + public ApiResponse> listLedgers( + @ModelAttribute SettlingSupportLedgerSearchParam param) { + return ApiResponse.ok(service.listLedgers(param)); + } + + @GetMapping("/ledgers/{ledgerId}") + @RequirePermission(menu = "SETTLING_SUPPORT", perm = "READ") + public ApiResponse 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 createLedger(@Valid @RequestBody SettlingSupportLedgerSaveReq req) { + return ApiResponse.ok(service.createLedger(req)); + } +} diff --git a/ga-api/src/main/java/com/ga/api/controller/commission/StepCloseController.java b/ga-api/src/main/java/com/ga/api/controller/commission/StepCloseController.java new file mode 100644 index 0000000..3b55822 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/controller/commission/StepCloseController.java @@ -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> list(@ModelAttribute StepCloseSearchParam param) { + return ApiResponse.ok(service.list(param)); + } + + @GetMapping("/by-month") + @RequirePermission(menu = "STEP_CLOSE", perm = "READ") + public ApiResponse> listByMonth(@RequestParam String settleMonth) { + return ApiResponse.ok(service.listByMonth(settleMonth)); + } + + @GetMapping("/{stepCloseId}") + @RequirePermission(menu = "STEP_CLOSE", perm = "READ") + public ApiResponse 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 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 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 reopen(@PathVariable Long stepCloseId) { + service.reopen(stepCloseId); + return ApiResponse.ok(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/AwardService.java b/ga-api/src/main/java/com/ga/api/service/commission/AwardService.java new file mode 100644 index 0000000..adc58fa --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/AwardService.java @@ -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 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 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(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/BranchAllowanceService.java b/ga-api/src/main/java/com/ga/api/service/commission/BranchAllowanceService.java new file mode 100644 index 0000000..d59cb07 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/BranchAllowanceService.java @@ -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 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 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(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/CommissionSimulationService.java b/ga-api/src/main/java/com/ga/api/service/commission/CommissionSimulationService.java new file mode 100644 index 0000000..6d77fe9 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/CommissionSimulationService.java @@ -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 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; + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/GradeEvaluationService.java b/ga-api/src/main/java/com/ga/api/service/commission/GradeEvaluationService.java new file mode 100644 index 0000000..437b9e9 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/GradeEvaluationService.java @@ -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 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 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); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/GuaranteeInsuranceService.java b/ga-api/src/main/java/com/ga/api/service/commission/GuaranteeInsuranceService.java new file mode 100644 index 0000000..8d90a0f --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/GuaranteeInsuranceService.java @@ -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 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); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/HqSettleService.java b/ga-api/src/main/java/com/ga/api/service/commission/HqSettleService.java new file mode 100644 index 0000000..6ee785a --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/HqSettleService.java @@ -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 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); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/IncomeCommissionService.java b/ga-api/src/main/java/com/ga/api/service/commission/IncomeCommissionService.java new file mode 100644 index 0000000..810d1d0 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/IncomeCommissionService.java @@ -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 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 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(); + } + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/JointContractService.java b/ga-api/src/main/java/com/ga/api/service/commission/JointContractService.java new file mode 100644 index 0000000..8655adf --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/JointContractService.java @@ -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 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을 초과합니다."); + } + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/LifeSupportService.java b/ga-api/src/main/java/com/ga/api/service/commission/LifeSupportService.java new file mode 100644 index 0000000..9cff5c1 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/LifeSupportService.java @@ -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 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 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(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/RecognizedPerformanceService.java b/ga-api/src/main/java/com/ga/api/service/commission/RecognizedPerformanceService.java new file mode 100644 index 0000000..48a3881 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/RecognizedPerformanceService.java @@ -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 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 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(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/ReferralCommissionService.java b/ga-api/src/main/java/com/ga/api/service/commission/ReferralCommissionService.java new file mode 100644 index 0000000..b93acc0 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/ReferralCommissionService.java @@ -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 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 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(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/ReserveFundService.java b/ga-api/src/main/java/com/ga/api/service/commission/ReserveFundService.java new file mode 100644 index 0000000..37b08da --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/ReserveFundService.java @@ -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 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(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/SettlingSupportService.java b/ga-api/src/main/java/com/ga/api/service/commission/SettlingSupportService.java new file mode 100644 index 0000000..f4ce553 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/SettlingSupportService.java @@ -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 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 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(); + } +} diff --git a/ga-api/src/main/java/com/ga/api/service/commission/StepCloseService.java b/ga-api/src/main/java/com/ga/api/service/commission/StepCloseService.java new file mode 100644 index 0000000..eca9af4 --- /dev/null +++ b/ga-api/src/main/java/com/ga/api/service/commission/StepCloseService.java @@ -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 list(StepCloseSearchParam param) { + param.startPage(); + return PageResponse.of(stepCloseMapper.selectList(param)); + } + + public List 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()); + } +} diff --git a/ga-api/src/test/java/com/ga/api/service/commission/AwardServiceTest.java b/ga-api/src/test/java/com/ga/api/service/commission/AwardServiceTest.java new file mode 100644 index 0000000..e3c1305 --- /dev/null +++ b/ga-api/src/test/java/com/ga/api/service/commission/AwardServiceTest.java @@ -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 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 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"); + } +} diff --git a/ga-api/src/test/java/com/ga/api/service/commission/BranchAllowanceServiceTest.java b/ga-api/src/test/java/com/ga/api/service/commission/BranchAllowanceServiceTest.java new file mode 100644 index 0000000..9d3edda --- /dev/null +++ b/ga-api/src/test/java/com/ga/api/service/commission/BranchAllowanceServiceTest.java @@ -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 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"); + } +} diff --git a/ga-api/src/test/java/com/ga/api/service/commission/CommissionSimulationServiceTest.java b/ga-api/src/test/java/com/ga/api/service/commission/CommissionSimulationServiceTest.java new file mode 100644 index 0000000..c954cbe --- /dev/null +++ b/ga-api/src/test/java/com/ga/api/service/commission/CommissionSimulationServiceTest.java @@ -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 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 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"); + } +} diff --git a/ga-api/src/test/java/com/ga/api/service/commission/GradeEvaluationServiceTest.java b/ga-api/src/test/java/com/ga/api/service/commission/GradeEvaluationServiceTest.java new file mode 100644 index 0000000..bded087 --- /dev/null +++ b/ga-api/src/test/java/com/ga/api/service/commission/GradeEvaluationServiceTest.java @@ -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 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 captor = ArgumentCaptor.forClass(AgentGradeEvaluationVO.class); + when(evalMapper.insert(captor.capture())).thenReturn(1); + + service.createEval(req); + + AgentGradeEvaluationVO saved = captor.getValue(); + assertThat(saved.getEvaluatedGradeId()).isEqualTo(2L); + } +} diff --git a/ga-api/src/test/java/com/ga/api/service/commission/HqSettleServiceTest.java b/ga-api/src/test/java/com/ga/api/service/commission/HqSettleServiceTest.java new file mode 100644 index 0000000..dc68b8b --- /dev/null +++ b/ga-api/src/test/java/com/ga/api/service/commission/HqSettleServiceTest.java @@ -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"); + } +} diff --git a/ga-api/src/test/java/com/ga/api/service/commission/IncomeCommissionServiceTest.java b/ga-api/src/test/java/com/ga/api/service/commission/IncomeCommissionServiceTest.java new file mode 100644 index 0000000..28a222c --- /dev/null +++ b/ga-api/src/test/java/com/ga/api/service/commission/IncomeCommissionServiceTest.java @@ -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 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 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"); + } +} diff --git a/ga-api/src/test/java/com/ga/api/service/commission/JointContractServiceTest.java b/ga-api/src/test/java/com/ga/api/service/commission/JointContractServiceTest.java new file mode 100644 index 0000000..a65dcf1 --- /dev/null +++ b/ga-api/src/test/java/com/ga/api/service/commission/JointContractServiceTest.java @@ -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); + } +} diff --git a/ga-api/src/test/java/com/ga/api/service/commission/LifeSupportServiceTest.java b/ga-api/src/test/java/com/ga/api/service/commission/LifeSupportServiceTest.java new file mode 100644 index 0000000..3dc0488 --- /dev/null +++ b/ga-api/src/test/java/com/ga/api/service/commission/LifeSupportServiceTest.java @@ -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 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 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"); + } +} diff --git a/ga-api/src/test/java/com/ga/api/service/commission/RecognizedPerformanceServiceTest.java b/ga-api/src/test/java/com/ga/api/service/commission/RecognizedPerformanceServiceTest.java new file mode 100644 index 0000000..3bc3dcd --- /dev/null +++ b/ga-api/src/test/java/com/ga/api/service/commission/RecognizedPerformanceServiceTest.java @@ -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 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"); + } +} diff --git a/ga-api/src/test/java/com/ga/api/service/commission/ReferralCommissionServiceTest.java b/ga-api/src/test/java/com/ga/api/service/commission/ReferralCommissionServiceTest.java new file mode 100644 index 0000000..4837dcc --- /dev/null +++ b/ga-api/src/test/java/com/ga/api/service/commission/ReferralCommissionServiceTest.java @@ -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 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 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"); + } +} diff --git a/ga-api/src/test/java/com/ga/api/service/commission/ReserveFundServiceTest.java b/ga-api/src/test/java/com/ga/api/service/commission/ReserveFundServiceTest.java new file mode 100644 index 0000000..1dca4a8 --- /dev/null +++ b/ga-api/src/test/java/com/ga/api/service/commission/ReserveFundServiceTest.java @@ -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 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 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"); + } +} diff --git a/ga-api/src/test/java/com/ga/api/service/commission/SettlingSupportServiceTest.java b/ga-api/src/test/java/com/ga/api/service/commission/SettlingSupportServiceTest.java new file mode 100644 index 0000000..2dd36e4 --- /dev/null +++ b/ga-api/src/test/java/com/ga/api/service/commission/SettlingSupportServiceTest.java @@ -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 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 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"); + } +} diff --git a/ga-batch/src/main/java/com/ga/batch/settlement/step/AggregateStep.java b/ga-batch/src/main/java/com/ga/batch/settlement/step/AggregateStep.java index 5b40494..4003c3d 100644 --- a/ga-batch/src/main/java/com/ga/batch/settlement/step/AggregateStep.java +++ b/ga-batch/src/main/java/com/ga/batch/settlement/step/AggregateStep.java @@ -4,6 +4,15 @@ import com.ga.batch.settlement.SettlementContext; import com.ga.batch.settlement.domain.BatchTaxCalculator; import com.ga.batch.settlement.domain.TaxBreakdown; import com.ga.common.util.MoneyUtil; +import com.ga.core.mapper.commission.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.MaintainLedgerMapper; import com.ga.core.mapper.ledger.RecruitLedgerMapper; @@ -68,6 +77,17 @@ public class AggregateStep implements Tasklet { private final CommissionStatementMapper statementMapper; 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 public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { log.info("[Step8 aggregate] settleMonth={}", ctx.getSettleMonth()); @@ -86,6 +106,17 @@ public class AggregateStep implements Tasklet { excMinus.put(agentId, toBd(row.get("minus"))); } + // 설계사 지급성 9종 집계 + Map collectionMap = toMap(collectionMapper.aggregateByAgent(ctx.getSettleMonth())); + Map renewalMap = toMap(renewalMapper.aggregateByAgent(ctx.getSettleMonth())); + Map reinstatementMap = toMap(reinstatementMapper.aggregateByAgent(ctx.getSettleMonth())); + Map persistencyMap = toMap(persistencyBonusMapper.aggregateByAgent(ctx.getSettleMonth())); + Map referralMap = toMap(referralMapper.aggregateByAgent(ctx.getSettleMonth())); + Map settlingSupportMap = toMap(settlingSupportMapper.aggregateByAgent(ctx.getSettleMonth())); + Map awardMap = toMap(awardMapper.aggregateByAgent(ctx.getSettleMonth())); + Map lifeSupportMap = toMap(lifeSupportMapper.aggregateByAgent(ctx.getSettleMonth())); + Map branchMgrMap = toMap(branchMgrAllowanceMapper.aggregateByAgent(ctx.getSettleMonth())); + // 합집합 agent_id Set agentIds = new HashSet<>(); agentIds.addAll(recruitMap.keySet()); @@ -94,6 +125,15 @@ public class AggregateStep implements Tasklet { agentIds.addAll(overrideMap.keySet()); agentIds.addAll(excPlus.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(); @@ -122,8 +162,21 @@ public class AggregateStep implements Tasklet { BigDecimal ep = excPlus.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) .subtract(cb).subtract(em); + gross = MoneyUtil.add(gross, otherCommission); // 세금유형 조회 (없으면 기본: BUSINESS_INCOME / NONE) AgentVO agentVO = agentMapper.selectById(agentId); @@ -152,6 +205,7 @@ public class AggregateStep implements Tasklet { vo.setExceptionMinus(em); vo.setOverrideTotal(ov); vo.setChargebackTotal(cb); + vo.setOtherCommissionTotal(otherCommission); vo.setGrossAmount(gross); vo.setTaxAmount(totalTax); // incomeTax + localTax 합산 vo.setNetAmount(netAmount); // gross - incomeTax - localTax + vat - deduction diff --git a/ga-common/src/main/resources/db/migration/V100__공동계약.sql b/ga-common/src/main/resources/db/migration/V100__공동계약.sql new file mode 100644 index 0000000..1bf09f1 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V100__공동계약.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V101__보증보험.sql b/ga-common/src/main/resources/db/migration/V101__보증보험.sql new file mode 100644 index 0000000..b16f982 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V101__보증보험.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V102__적립금.sql b/ga-common/src/main/resources/db/migration/V102__적립금.sql new file mode 100644 index 0000000..391ce65 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V102__적립금.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V103__수수료시뮬레이터.sql b/ga-common/src/main/resources/db/migration/V103__수수료시뮬레이터.sql new file mode 100644 index 0000000..a57dfc6 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V103__수수료시뮬레이터.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V104__incar운영_메뉴.sql b/ga-common/src/main/resources/db/migration/V104__incar운영_메뉴.sql new file mode 100644 index 0000000..84fc591 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V104__incar운영_메뉴.sql @@ -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; diff --git a/ga-common/src/main/resources/db/migration/V105__incar운영_공통코드.sql b/ga-common/src/main/resources/db/migration/V105__incar운영_공통코드.sql new file mode 100644 index 0000000..32843da --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V105__incar운영_공통코드.sql @@ -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; diff --git a/ga-common/src/main/resources/db/migration/V106__settle_master_기타수수료.sql b/ga-common/src/main/resources/db/migration/V106__settle_master_기타수수료.sql new file mode 100644 index 0000000..d204521 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V106__settle_master_기타수수료.sql @@ -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)'; diff --git a/ga-common/src/main/resources/db/migration/V88__수입수수료_수지차.sql b/ga-common/src/main/resources/db/migration/V88__수입수수료_수지차.sql new file mode 100644 index 0000000..8f468c5 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V88__수입수수료_수지차.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V89__소개이관수수료.sql b/ga-common/src/main/resources/db/migration/V89__소개이관수수료.sql new file mode 100644 index 0000000..0242356 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V89__소개이관수수료.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V90__정착지원금.sql b/ga-common/src/main/resources/db/migration/V90__정착지원금.sql new file mode 100644 index 0000000..b51624c --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V90__정착지원금.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V91__인정실적_환산율.sql b/ga-common/src/main/resources/db/migration/V91__인정실적_환산율.sql new file mode 100644 index 0000000..6c9ff79 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V91__인정실적_환산율.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V92__시상.sql b/ga-common/src/main/resources/db/migration/V92__시상.sql new file mode 100644 index 0000000..78e269e --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V92__시상.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V93__생보운영지원수수료.sql b/ga-common/src/main/resources/db/migration/V93__생보운영지원수수료.sql new file mode 100644 index 0000000..06cd5c2 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V93__생보운영지원수수료.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V94__지점장수당.sql b/ga-common/src/main/resources/db/migration/V94__지점장수당.sql new file mode 100644 index 0000000..4de60da --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V94__지점장수당.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V95__incar수수료_메뉴.sql b/ga-common/src/main/resources/db/migration/V95__incar수수료_메뉴.sql new file mode 100644 index 0000000..babe89b --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V95__incar수수료_메뉴.sql @@ -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; diff --git a/ga-common/src/main/resources/db/migration/V96__incar수수료_공통코드.sql b/ga-common/src/main/resources/db/migration/V96__incar수수료_공통코드.sql new file mode 100644 index 0000000..732ddea --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V96__incar수수료_공통코드.sql @@ -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; diff --git a/ga-common/src/main/resources/db/migration/V97__단계마감.sql b/ga-common/src/main/resources/db/migration/V97__단계마감.sql new file mode 100644 index 0000000..e79ed71 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V97__단계마감.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V98__본사정산.sql b/ga-common/src/main/resources/db/migration/V98__본사정산.sql new file mode 100644 index 0000000..1fc40cb --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V98__본사정산.sql @@ -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); diff --git a/ga-common/src/main/resources/db/migration/V99__등급평가.sql b/ga-common/src/main/resources/db/migration/V99__등급평가.sql new file mode 100644 index 0000000..17c4697 --- /dev/null +++ b/ga-common/src/main/resources/db/migration/V99__등급평가.sql @@ -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); diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/AgentGradeEvaluationMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/AgentGradeEvaluationMapper.java new file mode 100644 index 0000000..246be0d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/AgentGradeEvaluationMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/AgentGuaranteeInsuranceMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/AgentGuaranteeInsuranceMapper.java new file mode 100644 index 0000000..b729772 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/AgentGuaranteeInsuranceMapper.java @@ -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 selectList(AgentGuaranteeInsuranceSearchParam param); + + int insert(AgentGuaranteeInsuranceVO vo); + + int update(AgentGuaranteeInsuranceVO vo); + + int delete(@Param("guaranteeId") Long guaranteeId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/AgentReserveFundMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/AgentReserveFundMapper.java new file mode 100644 index 0000000..490dfe4 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/AgentReserveFundMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/AwardLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/AwardLedgerMapper.java new file mode 100644 index 0000000..fae07c7 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/AwardLedgerMapper.java @@ -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 selectList(AwardLedgerSearchParam param); + + /** 설계사 + 정산월 시상금 합계 */ + BigDecimal sumAwardByAgentAndMonth(@Param("agentId") Long agentId, + @Param("settleMonth") String settleMonth); + + /** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */ + List> 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/AwardRuleMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/AwardRuleMapper.java new file mode 100644 index 0000000..88ff46e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/AwardRuleMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/BranchMgrAllowanceLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/BranchMgrAllowanceLedgerMapper.java new file mode 100644 index 0000000..d65be96 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/BranchMgrAllowanceLedgerMapper.java @@ -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 selectList(BranchMgrAllowanceLedgerSearchParam param); + + /** 지점장 + 정산월 수당 합계 */ + BigDecimal sumAllowanceByManagerAndMonth(@Param("managerAgentId") Long managerAgentId, + @Param("settleMonth") String settleMonth); + + /** 정산월 기준 지점장별 순수당 합계(allowance_amount - COALESCE(clawback_amount,0)) — settle_master 집계 배치 용도 */ + List> 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/BranchMgrAllowanceRuleMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/BranchMgrAllowanceRuleMapper.java new file mode 100644 index 0000000..8632628 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/BranchMgrAllowanceRuleMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/CollectionCommissionLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/CollectionCommissionLedgerMapper.java index 0181d0e..976a69c 100644 --- a/ga-core/src/main/java/com/ga/core/mapper/commission/CollectionCommissionLedgerMapper.java +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/CollectionCommissionLedgerMapper.java @@ -8,6 +8,7 @@ import org.apache.ibatis.annotations.Param; import java.math.BigDecimal; import java.util.List; +import java.util.Map; /** * collection_commission_ledger Mapper. @@ -23,6 +24,9 @@ public interface CollectionCommissionLedgerMapper { BigDecimal sumByAgentAndMonth(@Param("agentId") Long agentId, @Param("settleMonth") String settleMonth); + /** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */ + List> aggregateByAgent(@Param("settleMonth") String settleMonth); + int insert(CollectionCommissionLedgerVO vo); int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status); diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/CommissionSimulationMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/CommissionSimulationMapper.java new file mode 100644 index 0000000..4be4c47 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/CommissionSimulationMapper.java @@ -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 selectList(CommissionSimulationSearchParam param); + + int insert(CommissionSimulationVO vo); + + int delete(@Param("simId") Long simId); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/GradeEvalRuleMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/GradeEvalRuleMapper.java new file mode 100644 index 0000000..fa0edb8 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/GradeEvalRuleMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/HqSettleItemMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/HqSettleItemMapper.java new file mode 100644 index 0000000..f3357b3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/HqSettleItemMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/IncomeCommissionLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/IncomeCommissionLedgerMapper.java new file mode 100644 index 0000000..5c45d2d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/IncomeCommissionLedgerMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/JointContractMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/JointContractMapper.java new file mode 100644 index 0000000..e8acc13 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/JointContractMapper.java @@ -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 selectList(JointContractSearchParam param); + + /** + * 계약별 공동설계사 목록 조회 — 지분율 확인용. + */ + List 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/LifeSupportLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/LifeSupportLedgerMapper.java new file mode 100644 index 0000000..5f36011 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/LifeSupportLedgerMapper.java @@ -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 selectList(LifeSupportLedgerSearchParam param); + + /** 설계사 + 정산월 실지급액 합계 */ + BigDecimal sumNetByAgentAndMonth(@Param("agentId") Long agentId, + @Param("settleMonth") String settleMonth); + + /** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */ + List> 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/LifeSupportRuleMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/LifeSupportRuleMapper.java new file mode 100644 index 0000000..693e987 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/LifeSupportRuleMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/MarginSummaryMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/MarginSummaryMapper.java new file mode 100644 index 0000000..9bca9c9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/MarginSummaryMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/PersistencyBonusMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/PersistencyBonusMapper.java index 22fedcb..d9680c0 100644 --- a/ga-core/src/main/java/com/ga/core/mapper/commission/PersistencyBonusMapper.java +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/PersistencyBonusMapper.java @@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import java.util.List; +import java.util.Map; /** * persistency_bonus Mapper. @@ -30,4 +31,7 @@ public interface PersistencyBonusMapper { int updateStatus(@Param("bonusId") Long bonusId, @Param("status") String status); int delete(@Param("bonusId") Long bonusId); + + /** 정산월 기준 설계사별 유지보너스 합계 — settle_master 집계 배치 용도 */ + List> aggregateByAgent(@Param("settleMonth") String settleMonth); } diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/ProductConversionRateMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/ProductConversionRateMapper.java new file mode 100644 index 0000000..317b39a --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/ProductConversionRateMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/RecognizedPerformanceLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/RecognizedPerformanceLedgerMapper.java new file mode 100644 index 0000000..aae7cdd --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/RecognizedPerformanceLedgerMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/ReferralCommissionLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/ReferralCommissionLedgerMapper.java new file mode 100644 index 0000000..3d2b2bf --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/ReferralCommissionLedgerMapper.java @@ -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 selectList(ReferralCommissionLedgerSearchParam param); + + /** 설계사 + 정산월 소개·이관수수료 합계 — commission_master 집계 배치 용도 */ + BigDecimal sumByAgentAndMonth(@Param("referrerAgentId") Long referrerAgentId, + @Param("settleMonth") String settleMonth); + + /** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */ + List> 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/ReferralCommissionRuleMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/ReferralCommissionRuleMapper.java new file mode 100644 index 0000000..972f1f9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/ReferralCommissionRuleMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/ReinstatementCommissionLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/ReinstatementCommissionLedgerMapper.java index d5048b9..6de4aa5 100644 --- a/ga-core/src/main/java/com/ga/core/mapper/commission/ReinstatementCommissionLedgerMapper.java +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/ReinstatementCommissionLedgerMapper.java @@ -8,6 +8,7 @@ import org.apache.ibatis.annotations.Param; import java.math.BigDecimal; import java.util.List; +import java.util.Map; @Mapper public interface ReinstatementCommissionLedgerMapper { @@ -15,6 +16,9 @@ public interface ReinstatementCommissionLedgerMapper { List selectList(ReinstatementCommissionLedgerSearchParam param); BigDecimal sumByAgentAndMonth(@Param("agentId") Long agentId, @Param("settleMonth") String settleMonth); + + /** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */ + List> aggregateByAgent(@Param("settleMonth") String settleMonth); int insert(ReinstatementCommissionLedgerVO vo); int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status); int delete(@Param("ledgerId") Long ledgerId); diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/RenewalCommissionLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/RenewalCommissionLedgerMapper.java index 9210c7c..969ec98 100644 --- a/ga-core/src/main/java/com/ga/core/mapper/commission/RenewalCommissionLedgerMapper.java +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/RenewalCommissionLedgerMapper.java @@ -8,6 +8,7 @@ import org.apache.ibatis.annotations.Param; import java.math.BigDecimal; import java.util.List; +import java.util.Map; @Mapper public interface RenewalCommissionLedgerMapper { @@ -15,6 +16,9 @@ public interface RenewalCommissionLedgerMapper { List selectList(RenewalCommissionLedgerSearchParam param); BigDecimal sumByAgentAndMonth(@Param("agentId") Long agentId, @Param("settleMonth") String settleMonth); + + /** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */ + List> aggregateByAgent(@Param("settleMonth") String settleMonth); int insert(RenewalCommissionLedgerVO vo); int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status); int delete(@Param("ledgerId") Long ledgerId); diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/SettlingSupportLedgerMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/SettlingSupportLedgerMapper.java new file mode 100644 index 0000000..502bcd8 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/SettlingSupportLedgerMapper.java @@ -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 selectList(SettlingSupportLedgerSearchParam param); + + /** 설계사 + 정산월 실지급액 합계 */ + BigDecimal sumNetByAgentAndMonth(@Param("agentId") Long agentId, + @Param("settleMonth") String settleMonth); + + /** 정산월 기준 설계사별 합계 — settle_master 집계 배치 용도 */ + List> 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/SettlingSupportRuleMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/SettlingSupportRuleMapper.java new file mode 100644 index 0000000..7d25e00 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/SettlingSupportRuleMapper.java @@ -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 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); +} diff --git a/ga-core/src/main/java/com/ga/core/mapper/commission/StepCloseMapper.java b/ga-core/src/main/java/com/ga/core/mapper/commission/StepCloseMapper.java new file mode 100644 index 0000000..5842e55 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/mapper/commission/StepCloseMapper.java @@ -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 selectList(StepCloseSearchParam param); + + /** 정산월 전체 단계 목록 (step_seq 순) */ + List 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); +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AgentGradeEvaluationResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGradeEvaluationResp.java new file mode 100644 index 0000000..bada89d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGradeEvaluationResp.java @@ -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; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AgentGradeEvaluationSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGradeEvaluationSaveReq.java new file mode 100644 index 0000000..262194d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGradeEvaluationSaveReq.java @@ -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; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AgentGradeEvaluationSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGradeEvaluationSearchParam.java new file mode 100644 index 0000000..1cdfc67 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGradeEvaluationSearchParam.java @@ -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; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AgentGradeEvaluationVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGradeEvaluationVO.java new file mode 100644 index 0000000..79db193 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGradeEvaluationVO.java @@ -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; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AgentGuaranteeInsuranceResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGuaranteeInsuranceResp.java new file mode 100644 index 0000000..4a16af2 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGuaranteeInsuranceResp.java @@ -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; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AgentGuaranteeInsuranceSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGuaranteeInsuranceSaveReq.java new file mode 100644 index 0000000..a0d45ef --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGuaranteeInsuranceSaveReq.java @@ -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; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AgentGuaranteeInsuranceSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGuaranteeInsuranceSearchParam.java new file mode 100644 index 0000000..7984a0a --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGuaranteeInsuranceSearchParam.java @@ -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; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AgentGuaranteeInsuranceVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGuaranteeInsuranceVO.java new file mode 100644 index 0000000..2c7e3b4 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AgentGuaranteeInsuranceVO.java @@ -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; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AgentReserveFundResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/AgentReserveFundResp.java new file mode 100644 index 0000000..2359a5e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AgentReserveFundResp.java @@ -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; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AgentReserveFundSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/AgentReserveFundSaveReq.java new file mode 100644 index 0000000..cd0ee96 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AgentReserveFundSaveReq.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * agent_reserve_fund 저장 요청. + * balance 는 Service가 직전월 잔액 조회 후 계산하여 채워 넣음. + */ +@Data +public class AgentReserveFundSaveReq { + + @NotNull + private Long agentId; + + @NotBlank + private String settleMonth; + + @NotNull + @DecimalMin("0") + private BigDecimal reserveIn; + + @NotNull + @DecimalMin("0") + private BigDecimal reserveOut; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AgentReserveFundSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/AgentReserveFundSearchParam.java new file mode 100644 index 0000000..da75c3c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AgentReserveFundSearchParam.java @@ -0,0 +1,16 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * agent_reserve_fund 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class AgentReserveFundSearchParam extends SearchParam { + private Long agentId; + private String agentName; + private String settleMonth; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AgentReserveFundVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/AgentReserveFundVO.java new file mode 100644 index 0000000..b2ef913 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AgentReserveFundVO.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * agent_reserve_fund — 적립금 (V102) + * 환수 대비 설계사별 월별 적립금 잔액. + * balance = (직전월 balance) + reserve_in - reserve_out (Service 계산). + */ +@Data +public class AgentReserveFundVO { + private Long reserveId; + /** 설계사 FK */ + private Long agentId; + /** 정산 귀속월 (YYYYMM) */ + private String settleMonth; + /** 당월 적립액 */ + private BigDecimal reserveIn; + /** 당월 인출/사용액 */ + private BigDecimal reserveOut; + /** 잔액 = 전월잔액 + reserve_in - reserve_out */ + private BigDecimal balance; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AwardLedgerResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/AwardLedgerResp.java new file mode 100644 index 0000000..567e3a9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AwardLedgerResp.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * award_ledger 조회 응답 — 설계사명·공통코드명 조인 포함. + */ +@Data +public class AwardLedgerResp { + private Long ledgerId; + private Long agentId; + /** agent.agent_name */ + private String agentName; + /** organization.org_name */ + private String orgName; + private String settleMonth; + private String awardType; + /** 공통코드 AWARD_TYPE */ + private String awardTypeName; + private BigDecimal baseAmount; + private BigDecimal awardAmount; + private String status; + /** 공통코드 INCOME_STATUS */ + private String statusName; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AwardLedgerSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/AwardLedgerSaveReq.java new file mode 100644 index 0000000..9a3e07c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AwardLedgerSaveReq.java @@ -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; + +/** + * award_ledger 등록 요청. + * award_amount 는 Service가 계산 후 VO에 set. + */ +@Data +public class AwardLedgerSaveReq { + + @NotNull + private Long agentId; + + @NotBlank + private String settleMonth; + + @NotBlank + private String awardType; + + @NotNull + @Positive + private BigDecimal baseAmount; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AwardLedgerSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/AwardLedgerSearchParam.java new file mode 100644 index 0000000..1bf9755 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AwardLedgerSearchParam.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * award_ledger 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class AwardLedgerSearchParam extends SearchParam { + private Long agentId; + private String agentName; + private String settleMonth; + private String awardType; + private String status; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AwardLedgerVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/AwardLedgerVO.java new file mode 100644 index 0000000..44007f1 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AwardLedgerVO.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * award_ledger — 시상 원장 (V92) + * award_amount = base_amount × award_rate (RATE) or award_fixed (FIXED) + */ +@Data +public class AwardLedgerVO { + private Long ledgerId; + private Long agentId; + /** 정산 귀속월 (YYYYMM) */ + private String settleMonth; + /** AwardType: NEW_RECRUIT / CORPORATE / RETENTION / PERFORMANCE */ + private String awardType; + /** 기준 실적 */ + private BigDecimal baseAmount; + /** 시상금 */ + private BigDecimal awardAmount; + /** CALCULATED / CONFIRMED / PAID / CANCELLED */ + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AwardRuleResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/AwardRuleResp.java new file mode 100644 index 0000000..2ef02cd --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AwardRuleResp.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * award_rule 조회 응답 — 공통코드명 조인 포함. + */ +@Data +public class AwardRuleResp { + private Long ruleId; + private String awardType; + /** 공통코드 AWARD_TYPE */ + private String awardTypeName; + private String calcType; + /** 공통코드 CALC_TYPE */ + private String calcTypeName; + private BigDecimal thresholdAmount; + private BigDecimal awardRate; + private BigDecimal awardFixed; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AwardRuleSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/AwardRuleSaveReq.java new file mode 100644 index 0000000..fae95a9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AwardRuleSaveReq.java @@ -0,0 +1,38 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * award_rule 등록/수정 요청. + */ +@Data +public class AwardRuleSaveReq { + + @NotBlank + private String awardType; + + @NotBlank + private String calcType; + + @DecimalMin("0") + private BigDecimal thresholdAmount; + + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal awardRate; + + @DecimalMin("0") + private BigDecimal awardFixed; + + @NotNull + private LocalDate effectiveFrom; + + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AwardRuleSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/AwardRuleSearchParam.java new file mode 100644 index 0000000..1c76e24 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AwardRuleSearchParam.java @@ -0,0 +1,15 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * award_rule 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class AwardRuleSearchParam extends SearchParam { + private String awardType; + private String calcType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AwardRuleVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/AwardRuleVO.java new file mode 100644 index 0000000..529e776 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AwardRuleVO.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * award_rule — 시상 룰 마스터 (V92) + */ +@Data +public class AwardRuleVO { + private Long ruleId; + /** AwardType: NEW_RECRUIT / CORPORATE / RETENTION / PERFORMANCE */ + private String awardType; + /** CalcType: RATE / FIXED */ + private String calcType; + /** 충족 기준 실적 (NULL=조건 없음) */ + private BigDecimal thresholdAmount; + /** 정률 시상율 (소수, calc_type=RATE 일 때 사용) */ + private BigDecimal awardRate; + /** 정액 시상금 (calc_type=FIXED 일 때 사용) */ + private BigDecimal awardFixed; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/AwardType.java b/ga-core/src/main/java/com/ga/core/vo/commission/AwardType.java new file mode 100644 index 0000000..fd2931a --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/AwardType.java @@ -0,0 +1,29 @@ +package com.ga.core.vo.commission; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 시상 유형 코드 (AWARD_TYPE 그룹, V96) + * award_rule.award_type, award_ledger.award_type 에서 사용. + */ +public enum AwardType { + NEW_RECRUIT("신인도입"), + CORPORATE("법인"), + RETENTION("유지율"), + PERFORMANCE("실적"); + + private final String label; + + AwardType(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceLedgerResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceLedgerResp.java new file mode 100644 index 0000000..b08d4b8 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceLedgerResp.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * branch_mgr_allowance_ledger 조회 응답 — 지점장명·조직명·공통코드명 조인 포함. + */ +@Data +public class BranchMgrAllowanceLedgerResp { + private Long ledgerId; + private Long managerAgentId; + /** agent.agent_name (지점장) */ + private String managerAgentName; + private Long orgId; + /** organization.org_name */ + private String orgName; + private String settleMonth; + private BigDecimal baseAmount; + private BigDecimal allowanceRate; + private BigDecimal allowanceAmount; + private BigDecimal clawbackAmount; + private String status; + /** 공통코드 SUPPORT_STATUS */ + private String statusName; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceLedgerSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceLedgerSaveReq.java new file mode 100644 index 0000000..091089b --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceLedgerSaveReq.java @@ -0,0 +1,39 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * branch_mgr_allowance_ledger 등록 요청. + * allowance_amount 는 Service가 계산(LEAST 적용) 후 VO에 set. + */ +@Data +public class BranchMgrAllowanceLedgerSaveReq { + + @NotNull + private Long managerAgentId; + + @NotNull + private Long orgId; + + @NotBlank + private String settleMonth; + + @NotNull + @Positive + private BigDecimal baseAmount; + + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal allowanceRate; + + @DecimalMin("0") + private BigDecimal clawbackAmount; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceLedgerSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceLedgerSearchParam.java new file mode 100644 index 0000000..a6656a5 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceLedgerSearchParam.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * branch_mgr_allowance_ledger 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class BranchMgrAllowanceLedgerSearchParam extends SearchParam { + private Long managerAgentId; + private String managerAgentName; + private Long orgId; + private String settleMonth; + private String status; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceLedgerVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceLedgerVO.java new file mode 100644 index 0000000..e759f53 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceLedgerVO.java @@ -0,0 +1,33 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * branch_mgr_allowance_ledger — 지점장수당 원장 (V94) + * allowance_amount = LEAST(base_amount × allowance_rate, cap_amount) + */ +@Data +public class BranchMgrAllowanceLedgerVO { + private Long ledgerId; + /** 지점장 설계사 FK */ + private Long managerAgentId; + /** 대상 조직 FK */ + private Long orgId; + /** 정산 귀속월 (YYYYMM) */ + private String settleMonth; + /** 조직 실적 기준금액 */ + private BigDecimal baseAmount; + /** 적용 수당율 (branch_mgr_allowance_rule 스냅샷) */ + private BigDecimal allowanceRate; + /** 수당액 = LEAST(base_amount × allowance_rate, cap_amount) */ + private BigDecimal allowanceAmount; + /** 환수액 */ + private BigDecimal clawbackAmount; + /** CALCULATED / CONFIRMED / PAID / CLAWED_BACK / CANCELLED */ + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceRuleResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceRuleResp.java new file mode 100644 index 0000000..de88c0e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceRuleResp.java @@ -0,0 +1,21 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * branch_mgr_allowance_rule 조회 응답 — 직책명 공통코드 조인 포함. + */ +@Data +public class BranchMgrAllowanceRuleResp { + private Long ruleId; + private String positionCode; + /** 공통코드 POSITION_CODE */ + private String positionCodeName; + private BigDecimal allowanceRate; + private BigDecimal capAmount; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceRuleSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceRuleSaveReq.java new file mode 100644 index 0000000..2a44c36 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceRuleSaveReq.java @@ -0,0 +1,34 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * branch_mgr_allowance_rule 등록/수정 요청. + */ +@Data +public class BranchMgrAllowanceRuleSaveReq { + + @NotBlank + private String positionCode; + + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal allowanceRate; + + /** NULL=무제한 */ + @DecimalMin("0") + private BigDecimal capAmount; + + @NotNull + private LocalDate effectiveFrom; + + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceRuleSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceRuleSearchParam.java new file mode 100644 index 0000000..7618336 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceRuleSearchParam.java @@ -0,0 +1,14 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * branch_mgr_allowance_rule 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class BranchMgrAllowanceRuleSearchParam extends SearchParam { + private String positionCode; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceRuleVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceRuleVO.java new file mode 100644 index 0000000..1adc26f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/BranchMgrAllowanceRuleVO.java @@ -0,0 +1,26 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * branch_mgr_allowance_rule — 지점장수당 룰 마스터 (V94) + */ +@Data +public class BranchMgrAllowanceRuleVO { + private Long ruleId; + /** 직책 코드 (지점장 등) */ + private String positionCode; + /** 수당율 (소수, 예: 0.02 = 2%) */ + private BigDecimal allowanceRate; + /** 수당 상한금액 (NULL=무제한) */ + private BigDecimal capAmount; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CalcType.java b/ga-core/src/main/java/com/ga/core/vo/commission/CalcType.java new file mode 100644 index 0000000..0e07bd3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CalcType.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.commission; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 계산방식 코드 (CALC_TYPE 그룹, V96) + * referral_commission_rule.calc_type, award_rule.calc_type 등에서 사용. + */ +public enum CalcType { + RATE("정률"), + FIXED("정액"); + + private final String label; + + CalcType(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CommissionSimulationResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionSimulationResp.java new file mode 100644 index 0000000..f36264d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionSimulationResp.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * commission_simulation 조회 응답. + * 시뮬레이터는 조인 대상이 없으므로 VO와 거의 동일하되 응답 전용 클래스로 분리. + */ +@Data +public class CommissionSimulationResp { + private Long simId; + private String simName; + private BigDecimal premiumAmount; + private BigDecimal companyRate; + private BigDecimal payoutRate; + private Integer commissionYear; + private BigDecimal companyPayment; + private BigDecimal agentCommission; + private BigDecimal withholdingTax; + private BigDecimal netAmount; + private BigDecimal expectedChargeback; + private LocalDateTime createdAt; + private Long createdBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CommissionSimulationSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionSimulationSaveReq.java new file mode 100644 index 0000000..4724458 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionSimulationSaveReq.java @@ -0,0 +1,43 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * commission_simulation 저장 요청. + * 산출 결과 컬럼(companyPayment/agentCommission/withholdingTax/netAmount)은 + * Service의 simulate() 메서드가 계산 후 VO에 채워 INSERT. + */ +@Data +public class CommissionSimulationSaveReq { + + @NotBlank + private String simName; + + @NotNull + @Positive + private BigDecimal premiumAmount; + + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal companyRate; + + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal payoutRate; + + @NotNull + @Positive + private Integer commissionYear; + + /** 예상 환수액 — 선택 입력 */ + private BigDecimal expectedChargeback; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CommissionSimulationSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionSimulationSearchParam.java new file mode 100644 index 0000000..697269b --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionSimulationSearchParam.java @@ -0,0 +1,15 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * commission_simulation 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class CommissionSimulationSearchParam extends SearchParam { + private String simName; + private Long createdBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CommissionSimulationVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionSimulationVO.java new file mode 100644 index 0000000..dc4d0f7 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionSimulationVO.java @@ -0,0 +1,37 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * commission_simulation — 수수료 시뮬레이터 (V103) + * 시나리오별 수수료 산출 결과 저장. + * 공식: + * 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 + */ +@Data +public class CommissionSimulationVO { + private Long simId; + private String simName; + private BigDecimal premiumAmount; + private BigDecimal companyRate; + private BigDecimal payoutRate; + private Integer commissionYear; + /** 보험사 지급액 = 보험료 × company_rate */ + private BigDecimal companyPayment; + /** 설계사 수수료 = 보험사지급액 × payout_rate */ + private BigDecimal agentCommission; + /** 원천세 = 설계사수수료 × 3.3% */ + private BigDecimal withholdingTax; + /** 실지급 = 설계사수수료 - 원천세 */ + private BigDecimal netAmount; + /** 예상 환수액 (시나리오에 따라 선택) */ + private BigDecimal expectedChargeback; + private LocalDateTime createdAt; + private Long createdBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/CommissionType.java b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionType.java index 2541b75..284b3e5 100644 --- a/ga-core/src/main/java/com/ga/core/vo/commission/CommissionType.java +++ b/ga-core/src/main/java/com/ga/core/vo/commission/CommissionType.java @@ -15,7 +15,15 @@ public enum CommissionType { REINSTATE("부활수수료"), PERSIST("유지보수수당"), INCENTIVE("시책수수료"), - ONBOARD("위촉수수료"); + ONBOARD("위촉수수료"), + INCOME("수입수수료"), + REFERRAL("소개수수료"), + TRANSFER("이관수수료"), + SETTLING("정착지원금"), + RECOGNIZED("인정실적"), + AWARD("시상"), + LIFE_SUPPORT("생보운영지원수수료"), + BRANCH_ALLOWANCE("지점장수당"); private final String label; diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/GradeEvalRuleResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/GradeEvalRuleResp.java new file mode 100644 index 0000000..51cf42a --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/GradeEvalRuleResp.java @@ -0,0 +1,21 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * grade_eval_rule 조회 응답 — 등급명 조인 포함. + */ +@Data +public class GradeEvalRuleResp { + private Long ruleId; + private Long gradeId; + private String gradeName; + private BigDecimal minScore; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/GradeEvalRuleSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/GradeEvalRuleSaveReq.java new file mode 100644 index 0000000..b9c086c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/GradeEvalRuleSaveReq.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * grade_eval_rule 저장 요청. + */ +@Data +public class GradeEvalRuleSaveReq { + + @NotNull + private Long gradeId; + + @NotNull + private BigDecimal minScore; + + @NotNull + private LocalDate effectiveFrom; + + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/GradeEvalRuleSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/GradeEvalRuleSearchParam.java new file mode 100644 index 0000000..c6d13fd --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/GradeEvalRuleSearchParam.java @@ -0,0 +1,16 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * grade_eval_rule 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class GradeEvalRuleSearchParam extends SearchParam { + private Long gradeId; + /** 적용 기준일 (이 날짜가 effective_from~effective_to 범위 안에 있는 룰만 조회) */ + private String baseDate; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/GradeEvalRuleVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/GradeEvalRuleVO.java new file mode 100644 index 0000000..80fb9c7 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/GradeEvalRuleVO.java @@ -0,0 +1,24 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * grade_eval_rule — 등급평가 룰 마스터 (V99) + * 점수 구간별 등급 매핑. min_score 이상이면 해당 grade 부여. + */ +@Data +public class GradeEvalRuleVO { + private Long ruleId; + /** 부여 등급 FK (grade.grade_id) */ + private Long gradeId; + /** 최소 실적점수 (이 점수 이상이면 해당 등급) */ + private BigDecimal minScore; + private LocalDate effectiveFrom; + /** NULL = 현재 유효 */ + private LocalDate effectiveTo; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/HqItemType.java b/ga-core/src/main/java/com/ga/core/vo/commission/HqItemType.java new file mode 100644 index 0000000..b348bba --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/HqItemType.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.commission; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 본사정산 항목유형 (HQ_ITEM_TYPE 그룹, V98) + * hq_settle_item.item_type 에서 사용. + * 본사손익 = Σ(INCOME) − Σ(EXPENSE) + */ +public enum HqItemType { + INCOME("수입"), + EXPENSE("비용"); + + private final String label; + + HqItemType(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/HqSettleItemResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/HqSettleItemResp.java new file mode 100644 index 0000000..3e2b162 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/HqSettleItemResp.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * hq_settle_item 조회 응답 — 코드명 포함. + */ +@Data +public class HqSettleItemResp { + private Long itemId; + private String settleMonth; + private String itemType; + private String itemTypeName; + private String itemName; + private BigDecimal amount; + private String memo; + private String status; + private String statusName; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/HqSettleItemSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/HqSettleItemSaveReq.java new file mode 100644 index 0000000..79ddded --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/HqSettleItemSaveReq.java @@ -0,0 +1,31 @@ +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; + +/** + * hq_settle_item 저장 요청. + * status/createdAt/updatedAt 는 서버에서 결정. + */ +@Data +public class HqSettleItemSaveReq { + + @NotBlank + private String settleMonth; + + @NotBlank + private String itemType; + + @NotBlank + private String itemName; + + @NotNull + @Positive + private BigDecimal amount; + + private String memo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/HqSettleItemSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/HqSettleItemSearchParam.java new file mode 100644 index 0000000..c04941f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/HqSettleItemSearchParam.java @@ -0,0 +1,15 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * hq_settle_item 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class HqSettleItemSearchParam extends SearchParam { + private String settleMonth; + private String itemType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/HqSettleItemVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/HqSettleItemVO.java new file mode 100644 index 0000000..7fb8f00 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/HqSettleItemVO.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * hq_settle_item — 본사정산 항목 (V98) + * GA 자체 수입/비용 정산. 본사손익 = Σ(INCOME) − Σ(EXPENSE). + */ +@Data +public class HqSettleItemVO { + private Long itemId; + /** 정산 귀속월 (YYYYMM) */ + private String settleMonth; + /** 항목유형 (INCOME=수입/EXPENSE=비용) */ + private String itemType; + private String itemName; + private BigDecimal amount; + private String memo; + /** REGISTERED=등록/CONFIRMED=확정 */ + private String status; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/IncomeCommissionLedgerResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/IncomeCommissionLedgerResp.java new file mode 100644 index 0000000..b2cb72f --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/IncomeCommissionLedgerResp.java @@ -0,0 +1,29 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * income_commission_ledger 조회 응답 — 보험사명·계약번호 조인 포함. + */ +@Data +public class IncomeCommissionLedgerResp { + private Long incomeId; + private Long contractId; + /** contract.policy_no */ + private String policyNo; + private Long carrierId; + /** insurance_company.company_name */ + private String carrierName; + private String settleMonth; + private BigDecimal premiumAmount; + private BigDecimal companyRate; + private BigDecimal incomeAmount; + private String commissionType; + /** 공통코드 COMMISSION_TYPE */ + private String commissionTypeName; + private String status; + /** 공통코드 INCOME_STATUS */ + private String statusName; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/IncomeCommissionLedgerSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/IncomeCommissionLedgerSaveReq.java new file mode 100644 index 0000000..be1afc1 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/IncomeCommissionLedgerSaveReq.java @@ -0,0 +1,37 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +/** + * income_commission_ledger 등록/수정 요청. + * income_amount는 Service가 계산 후 VO에 set하므로 요청에 포함하지 않는다. + */ +@Data +public class IncomeCommissionLedgerSaveReq { + + @NotNull + private Long contractId; + + @NotNull + private Long carrierId; + + @NotBlank + private String settleMonth; + + @NotNull + @Positive + private java.math.BigDecimal premiumAmount; + + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private java.math.BigDecimal companyRate; + + @NotBlank + private String commissionType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/IncomeCommissionLedgerSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/IncomeCommissionLedgerSearchParam.java new file mode 100644 index 0000000..e7d3bd4 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/IncomeCommissionLedgerSearchParam.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * income_commission_ledger 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class IncomeCommissionLedgerSearchParam extends SearchParam { + private Long carrierId; + private Long contractId; + private String settleMonth; + private String commissionType; + private String status; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/IncomeCommissionLedgerVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/IncomeCommissionLedgerVO.java new file mode 100644 index 0000000..28f5776 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/IncomeCommissionLedgerVO.java @@ -0,0 +1,31 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * income_commission_ledger — 수입수수료 원장 (V88) + * 보험사가 GA에 지급한 수입 측 수수료. income_amount = premium_amount × company_rate. + */ +@Data +public class IncomeCommissionLedgerVO { + private Long incomeId; + private Long contractId; + /** 보험사 FK (insurance_company.company_id) */ + private Long carrierId; + /** 정산 귀속월 (YYYYMM) */ + private String settleMonth; + private BigDecimal premiumAmount; + /** 보험사 수수료율 (소수, 예: 0.12 = 12%) */ + private BigDecimal companyRate; + /** 수입수수료 = premium_amount × company_rate */ + private BigDecimal incomeAmount; + /** CommissionType: RECRUIT/MAINTAIN/RENEWAL/REINSTATE/COLLECTION */ + private String commissionType; + /** CALCULATED / CONFIRMED / CANCELLED */ + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/JointContractResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/JointContractResp.java new file mode 100644 index 0000000..77d07e3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/JointContractResp.java @@ -0,0 +1,24 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * joint_contract 조회 응답 — 설계사명/계약번호 조인 포함. + */ +@Data +public class JointContractResp { + private Long jointId; + private Long contractId; + private String policyNo; + private Long agentId; + private String agentName; + private String orgName; + private String role; + private String roleName; + private BigDecimal shareRate; + private LocalDateTime createdAt; + private Long createdBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/JointContractSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/JointContractSaveReq.java new file mode 100644 index 0000000..3d25a68 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/JointContractSaveReq.java @@ -0,0 +1,31 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * joint_contract 저장 요청. + * share_rate 합=1 은 Service 레이어에서 검증. + */ +@Data +public class JointContractSaveReq { + + @NotNull + private Long contractId; + + @NotNull + private Long agentId; + + @NotBlank + private String role; + + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal shareRate; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/JointContractSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/JointContractSearchParam.java new file mode 100644 index 0000000..d515829 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/JointContractSearchParam.java @@ -0,0 +1,17 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * joint_contract 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class JointContractSearchParam extends SearchParam { + private Long contractId; + private Long agentId; + private String agentName; + private String role; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/JointContractVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/JointContractVO.java new file mode 100644 index 0000000..1c33603 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/JointContractVO.java @@ -0,0 +1,26 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * joint_contract — 공동계약 (V100) + * 한 계약을 여러 설계사가 공동 모집. share_rate 합=1 (Service 검증). + * 설계사몫 = 계약수수료 × share_rate + */ +@Data +public class JointContractVO { + private Long jointId; + /** 대상 계약 FK */ + private Long contractId; + /** 참여 설계사 FK */ + private Long agentId; + /** 역할 (LEAD=주설계사/SUB=공동설계사) */ + private String role; + /** 지분율 0~1 */ + private BigDecimal shareRate; + private LocalDateTime createdAt; + private Long createdBy; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/JointRole.java b/ga-core/src/main/java/com/ga/core/vo/commission/JointRole.java new file mode 100644 index 0000000..b514219 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/JointRole.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.commission; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 공동계약 역할 (JOINT_ROLE 그룹, V100) + * joint_contract.role 에서 사용. + * 계약별 share_rate 합 = 1 (Service 검증). + */ +public enum JointRole { + LEAD("주설계사"), + SUB("공동설계사"); + + private final String label; + + JointRole(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportLedgerResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportLedgerResp.java new file mode 100644 index 0000000..fb048d7 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportLedgerResp.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * life_support_ledger 조회 응답 — 설계사명·조직명·공통코드명 조인 포함. + */ +@Data +public class LifeSupportLedgerResp { + private Long ledgerId; + private Long agentId; + /** agent.agent_name */ + private String agentName; + /** organization.org_name */ + private String orgName; + private String settleMonth; + private BigDecimal baseAmount; + private BigDecimal supportRate; + private BigDecimal supportAmount; + private BigDecimal clawbackAmount; + private BigDecimal netAmount; + private String status; + /** 공통코드 SUPPORT_STATUS */ + private String statusName; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportLedgerSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportLedgerSaveReq.java new file mode 100644 index 0000000..4914dec --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportLedgerSaveReq.java @@ -0,0 +1,36 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * life_support_ledger 등록 요청. + * support_amount / net_amount 는 Service가 계산 후 VO에 set. + */ +@Data +public class LifeSupportLedgerSaveReq { + + @NotNull + private Long agentId; + + @NotBlank + private String settleMonth; + + @NotNull + @Positive + private BigDecimal baseAmount; + + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal supportRate; + + @DecimalMin("0") + private BigDecimal clawbackAmount; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportLedgerSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportLedgerSearchParam.java new file mode 100644 index 0000000..2569f64 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportLedgerSearchParam.java @@ -0,0 +1,17 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * life_support_ledger 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class LifeSupportLedgerSearchParam extends SearchParam { + private Long agentId; + private String agentName; + private String settleMonth; + private String status; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportLedgerVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportLedgerVO.java new file mode 100644 index 0000000..3b24fba --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportLedgerVO.java @@ -0,0 +1,33 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * life_support_ledger — 생보운영지원수수료 원장 (V93) + * support_amount = base_amount × support_rate + * net_amount = support_amount - clawback_amount + */ +@Data +public class LifeSupportLedgerVO { + private Long ledgerId; + private Long agentId; + /** 정산 귀속월 (YYYYMM) */ + private String settleMonth; + /** 생보 실적 기준금액 */ + private BigDecimal baseAmount; + /** 적용 지급률 (life_support_rule 스냅샷) */ + private BigDecimal supportRate; + /** 지원수수료 = base_amount × support_rate */ + private BigDecimal supportAmount; + /** 환수액 */ + private BigDecimal clawbackAmount; + /** 실지급액 = support_amount - clawback_amount */ + private BigDecimal netAmount; + /** CALCULATED / CONFIRMED / PAID / CLAWED_BACK / CANCELLED */ + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportRuleResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportRuleResp.java new file mode 100644 index 0000000..e7fea7e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportRuleResp.java @@ -0,0 +1,20 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * life_support_rule 조회 응답 — 등급명 공통코드 조인 포함. + */ +@Data +public class LifeSupportRuleResp { + private Long ruleId; + private String agentGrade; + /** 공통코드 AGENT_GRADE */ + private String agentGradeName; + private BigDecimal supportRate; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportRuleSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportRuleSaveReq.java new file mode 100644 index 0000000..29a4a41 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportRuleSaveReq.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * life_support_rule 등록/수정 요청. + */ +@Data +public class LifeSupportRuleSaveReq { + + @NotBlank + private String agentGrade; + + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal supportRate; + + @NotNull + private LocalDate effectiveFrom; + + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportRuleSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportRuleSearchParam.java new file mode 100644 index 0000000..e809988 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportRuleSearchParam.java @@ -0,0 +1,14 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * life_support_rule 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class LifeSupportRuleSearchParam extends SearchParam { + private String agentGrade; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportRuleVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportRuleVO.java new file mode 100644 index 0000000..f54d710 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/LifeSupportRuleVO.java @@ -0,0 +1,24 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * life_support_rule — 생보운영지원수수료 룰 마스터 (V93) + */ +@Data +public class LifeSupportRuleVO { + private Long ruleId; + /** 설계사 등급 */ + private String agentGrade; + /** 지급률 (소수, 예: 0.03 = 3%) */ + private BigDecimal supportRate; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/MarginSummaryResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/MarginSummaryResp.java new file mode 100644 index 0000000..00ea0b5 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/MarginSummaryResp.java @@ -0,0 +1,21 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * margin_summary 조회 응답 — 설계사명 조인 포함. + */ +@Data +public class MarginSummaryResp { + private Long marginId; + private String settleMonth; + private Long agentId; + /** agent.agent_name (NULL = 전체) */ + private String agentName; + private BigDecimal incomeTotal; + private BigDecimal payoutTotal; + private BigDecimal marginAmount; + private BigDecimal marginRate; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/MarginSummarySaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/MarginSummarySaveReq.java new file mode 100644 index 0000000..e0a111b --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/MarginSummarySaveReq.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.NotBlank; +import lombok.Data; + +/** + * margin_summary 등록/수정 요청. + * margin_amount / margin_rate 는 Service가 계산 후 VO에 set. + */ +@Data +public class MarginSummarySaveReq { + + @NotBlank + private String settleMonth; + + /** 설계사 FK (NULL = GA 전체 합산) */ + private Long agentId; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/MarginSummarySearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/MarginSummarySearchParam.java new file mode 100644 index 0000000..2b33ba9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/MarginSummarySearchParam.java @@ -0,0 +1,15 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * margin_summary 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class MarginSummarySearchParam extends SearchParam { + private String settleMonth; + private Long agentId; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/MarginSummaryVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/MarginSummaryVO.java new file mode 100644 index 0000000..81f88d6 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/MarginSummaryVO.java @@ -0,0 +1,29 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * margin_summary — 수지차 집계 (V88) + * recalc 배치 액션으로 채움. agent_id NULL = GA 전체 합산. + * margin_amount = income_total - payout_total + */ +@Data +public class MarginSummaryVO { + private Long marginId; + /** 정산월 (YYYYMM) */ + private String settleMonth; + /** 설계사 FK (NULL = GA 전체) */ + private Long agentId; + /** 수입 합계 (income_commission_ledger SUM) */ + private BigDecimal incomeTotal; + /** 지급 합계 (settle_master.net_amount SUM) */ + private BigDecimal payoutTotal; + /** 수지차 = income_total - payout_total */ + private BigDecimal marginAmount; + /** 수지차율 = margin_amount / income_total */ + private BigDecimal marginRate; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ProductConversionRateResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/ProductConversionRateResp.java new file mode 100644 index 0000000..31f0aa3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ProductConversionRateResp.java @@ -0,0 +1,20 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * product_conversion_rate 조회 응답 — 상품명 조인 포함. + */ +@Data +public class ProductConversionRateResp { + private Long rateId; + private Long productId; + /** product.product_name */ + private String productName; + private BigDecimal conversionRate; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ProductConversionRateSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/ProductConversionRateSaveReq.java new file mode 100644 index 0000000..88d5e70 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ProductConversionRateSaveReq.java @@ -0,0 +1,29 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * product_conversion_rate 등록/수정 요청. + */ +@Data +public class ProductConversionRateSaveReq { + + @NotNull + private Long productId; + + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal conversionRate; + + @NotNull + private LocalDate effectiveFrom; + + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ProductConversionRateSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/ProductConversionRateSearchParam.java new file mode 100644 index 0000000..8bdd188 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ProductConversionRateSearchParam.java @@ -0,0 +1,14 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * product_conversion_rate 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ProductConversionRateSearchParam extends SearchParam { + private Long productId; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ProductConversionRateVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/ProductConversionRateVO.java new file mode 100644 index 0000000..f1ab307 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ProductConversionRateVO.java @@ -0,0 +1,24 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * product_conversion_rate — 상품별 환산율 마스터 (V91) + * 인정실적 산출을 위한 보험료 환산율. + */ +@Data +public class ProductConversionRateVO { + private Long rateId; + private Long productId; + /** 환산율 (소수, 예: 0.5 = 50%) */ + private BigDecimal conversionRate; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RecognizedPerformanceLedgerResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/RecognizedPerformanceLedgerResp.java new file mode 100644 index 0000000..eed315e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RecognizedPerformanceLedgerResp.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * recognized_performance_ledger 조회 응답 — 설계사명·계약번호 조인 포함. + */ +@Data +public class RecognizedPerformanceLedgerResp { + private Long ledgerId; + private Long agentId; + /** agent.agent_name */ + private String agentName; + /** organization.org_name */ + private String orgName; + private Long contractId; + /** contract.policy_no */ + private String policyNo; + private String settleMonth; + private BigDecimal premiumAmount; + private BigDecimal conversionRate; + private BigDecimal convertedPremium; + private BigDecimal recognitionRate; + private BigDecimal recognizedAmount; + private String status; + /** 공통코드 INCOME_STATUS */ + private String statusName; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RecognizedPerformanceLedgerSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/RecognizedPerformanceLedgerSaveReq.java new file mode 100644 index 0000000..168d294 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RecognizedPerformanceLedgerSaveReq.java @@ -0,0 +1,41 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * recognized_performance_ledger 등록 요청. + * converted_premium / recognized_amount 는 Service가 계산 후 VO에 set. + */ +@Data +public class RecognizedPerformanceLedgerSaveReq { + + @NotNull + private Long agentId; + + @NotNull + private Long contractId; + + @NotBlank + private String settleMonth; + + @NotNull + @Positive + private BigDecimal premiumAmount; + + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal conversionRate; + + @NotNull + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal recognitionRate; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RecognizedPerformanceLedgerSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/RecognizedPerformanceLedgerSearchParam.java new file mode 100644 index 0000000..e398593 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RecognizedPerformanceLedgerSearchParam.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * recognized_performance_ledger 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class RecognizedPerformanceLedgerSearchParam extends SearchParam { + private Long agentId; + private String agentName; + private Long contractId; + private String settleMonth; + private String status; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/RecognizedPerformanceLedgerVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/RecognizedPerformanceLedgerVO.java new file mode 100644 index 0000000..a5bc715 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/RecognizedPerformanceLedgerVO.java @@ -0,0 +1,33 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * recognized_performance_ledger — 인정실적 원장 (V91) + * converted_premium = premium_amount × conversion_rate + * recognized_amount = converted_premium × recognition_rate + */ +@Data +public class RecognizedPerformanceLedgerVO { + private Long ledgerId; + private Long agentId; + private Long contractId; + /** 정산 귀속월 (YYYYMM) */ + private String settleMonth; + private BigDecimal premiumAmount; + /** 적용 환산율 (product_conversion_rate 스냅샷) */ + private BigDecimal conversionRate; + /** 환산보험료 = premium_amount × conversion_rate */ + private BigDecimal convertedPremium; + /** 인정율 (설계사 등급/정책 기준) */ + private BigDecimal recognitionRate; + /** 인정실적 = converted_premium × recognition_rate */ + private BigDecimal recognizedAmount; + /** CALCULATED / CONFIRMED / CANCELLED */ + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionLedgerResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionLedgerResp.java new file mode 100644 index 0000000..3fe9fd6 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionLedgerResp.java @@ -0,0 +1,29 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * referral_commission_ledger 조회 응답 — 설계사명·계약번호·공통코드명 조인 포함. + */ +@Data +public class ReferralCommissionLedgerResp { + private Long ledgerId; + private Long contractId; + /** contract.policy_no */ + private String policyNo; + private Long referrerAgentId; + /** agent.agent_name */ + private String referrerAgentName; + private String settleMonth; + private String referralType; + /** 공통코드 REFERRAL_TYPE */ + private String referralTypeName; + private BigDecimal baseAmount; + private BigDecimal commissionRate; + private BigDecimal commissionAmount; + private String status; + /** 공통코드 INCOME_STATUS */ + private String statusName; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionLedgerSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionLedgerSaveReq.java new file mode 100644 index 0000000..88ef6dd --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionLedgerSaveReq.java @@ -0,0 +1,38 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * referral_commission_ledger 등록 요청. + * commission_amount 는 Service가 계산 후 VO에 set. + */ +@Data +public class ReferralCommissionLedgerSaveReq { + + @NotNull + private Long contractId; + + @NotNull + private Long referrerAgentId; + + @NotBlank + private String settleMonth; + + @NotBlank + private String referralType; + + @NotNull + @Positive + private BigDecimal baseAmount; + + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal commissionRate; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionLedgerSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionLedgerSearchParam.java new file mode 100644 index 0000000..d71f5b9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionLedgerSearchParam.java @@ -0,0 +1,18 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * referral_commission_ledger 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ReferralCommissionLedgerSearchParam extends SearchParam { + private Long referrerAgentId; + private Long contractId; + private String settleMonth; + private String referralType; + private String status; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionLedgerVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionLedgerVO.java new file mode 100644 index 0000000..9e714c3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionLedgerVO.java @@ -0,0 +1,32 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * referral_commission_ledger — 소개·이관수수료 원장 (V89) + * commission_amount = base_amount × commission_rate (RATE) or fixed_amount (FIXED) + */ +@Data +public class ReferralCommissionLedgerVO { + private Long ledgerId; + private Long contractId; + /** 소개/이관한 설계사 FK */ + private Long referrerAgentId; + /** 정산 귀속월 (YYYYMM) */ + private String settleMonth; + /** ReferralType: REFERRAL / TRANSFER */ + private String referralType; + /** 기준금액 (수수료 또는 보험료) */ + private BigDecimal baseAmount; + /** 적용 정률 요율 (RATE 방식 시) */ + private BigDecimal commissionRate; + /** 수수료액 */ + private BigDecimal commissionAmount; + /** CALCULATED / CONFIRMED / CANCELLED */ + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionRuleResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionRuleResp.java new file mode 100644 index 0000000..d68d65e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionRuleResp.java @@ -0,0 +1,24 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * referral_commission_rule 조회 응답 — 공통코드명 조인 포함. + */ +@Data +public class ReferralCommissionRuleResp { + private Long ruleId; + private String referralType; + /** 공통코드 REFERRAL_TYPE */ + private String referralTypeName; + private String calcType; + /** 공통코드 CALC_TYPE */ + private String calcTypeName; + private BigDecimal commissionRate; + private BigDecimal fixedAmount; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionRuleSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionRuleSaveReq.java new file mode 100644 index 0000000..f15b027 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionRuleSaveReq.java @@ -0,0 +1,35 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * referral_commission_rule 등록/수정 요청. + */ +@Data +public class ReferralCommissionRuleSaveReq { + + @NotBlank + private String referralType; + + @NotBlank + private String calcType; + + @DecimalMin("0") + @DecimalMax("1") + private BigDecimal commissionRate; + + @DecimalMin("0") + private BigDecimal fixedAmount; + + @NotNull + private LocalDate effectiveFrom; + + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionRuleSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionRuleSearchParam.java new file mode 100644 index 0000000..2fb4bb9 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionRuleSearchParam.java @@ -0,0 +1,15 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * referral_commission_rule 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ReferralCommissionRuleSearchParam extends SearchParam { + private String referralType; + private String calcType; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionRuleVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionRuleVO.java new file mode 100644 index 0000000..c97cc1e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralCommissionRuleVO.java @@ -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; + +/** + * referral_commission_rule — 소개·이관수수료 룰 마스터 (V89) + */ +@Data +public class ReferralCommissionRuleVO { + private Long ruleId; + /** ReferralType: REFERRAL / TRANSFER */ + private String referralType; + /** CalcType: RATE / FIXED */ + private String calcType; + /** 정률 요율 (소수, calc_type=RATE 일 때 사용) */ + private BigDecimal commissionRate; + /** 정액 금액 (calc_type=FIXED 일 때 사용) */ + private BigDecimal fixedAmount; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/ReferralType.java b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralType.java new file mode 100644 index 0000000..8f787cd --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/ReferralType.java @@ -0,0 +1,27 @@ +package com.ga.core.vo.commission; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 소개·이관 유형 코드 (REFERRAL_TYPE 그룹, V89) + * referral_commission_rule.referral_type, referral_commission_ledger.referral_type 에서 사용. + */ +public enum ReferralType { + REFERRAL("소개"), + TRANSFER("이관"); + + private final String label; + + ReferralType(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportLedgerResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportLedgerResp.java new file mode 100644 index 0000000..57944d3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportLedgerResp.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * settling_support_ledger 조회 응답 — 설계사명·조직명·공통코드명 조인 포함. + */ +@Data +public class SettlingSupportLedgerResp { + private Long ledgerId; + private Long agentId; + /** agent.agent_name */ + private String agentName; + /** organization.org_name */ + private String orgName; + private String settleMonth; + private BigDecimal supportAmount; + private BigDecimal clawbackAmount; + private BigDecimal netAmount; + private String status; + /** 공통코드 SUPPORT_STATUS */ + private String statusName; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportLedgerSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportLedgerSaveReq.java new file mode 100644 index 0000000..0f1333e --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportLedgerSaveReq.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * settling_support_ledger 등록 요청. + * net_amount 는 Service가 계산 후 VO에 set. + */ +@Data +public class SettlingSupportLedgerSaveReq { + + @NotNull + private Long agentId; + + @NotBlank + private String settleMonth; + + @NotNull + @Positive + private BigDecimal supportAmount; + + @DecimalMin("0") + private BigDecimal clawbackAmount; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportLedgerSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportLedgerSearchParam.java new file mode 100644 index 0000000..912fb3c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportLedgerSearchParam.java @@ -0,0 +1,17 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * settling_support_ledger 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SettlingSupportLedgerSearchParam extends SearchParam { + private Long agentId; + private String agentName; + private String settleMonth; + private String status; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportLedgerVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportLedgerVO.java new file mode 100644 index 0000000..3c08fe1 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportLedgerVO.java @@ -0,0 +1,28 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * settling_support_ledger — 정착지원금 원장 (V90) + * net_amount = support_amount - clawback_amount + */ +@Data +public class SettlingSupportLedgerVO { + private Long ledgerId; + private Long agentId; + /** 정산 귀속월 (YYYYMM) */ + private String settleMonth; + /** 지급액 */ + private BigDecimal supportAmount; + /** 환수액 (조기해촉 시 support_amount × 잔여약정개월/약정개월) */ + private BigDecimal clawbackAmount; + /** 실지급액 = support_amount - clawback_amount */ + private BigDecimal netAmount; + /** CALCULATED / PAID / CLAWED_BACK / CANCELLED */ + private String status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportRuleResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportRuleResp.java new file mode 100644 index 0000000..e1d2fc6 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportRuleResp.java @@ -0,0 +1,22 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; + +/** + * settling_support_rule 조회 응답 — 등급명 공통코드 조인 포함. + */ +@Data +public class SettlingSupportRuleResp { + private Long ruleId; + private String agentGrade; + /** 공통코드 AGENT_GRADE */ + private String agentGradeName; + private BigDecimal supportAmount; + private Integer durationMonths; + private Integer clawbackMonths; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportRuleSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportRuleSaveReq.java new file mode 100644 index 0000000..0dc9c50 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportRuleSaveReq.java @@ -0,0 +1,37 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.Min; +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; + +/** + * settling_support_rule 등록/수정 요청. + */ +@Data +public class SettlingSupportRuleSaveReq { + + @NotBlank + private String agentGrade; + + @NotNull + @Positive + private BigDecimal supportAmount; + + @NotNull + @Positive + private Integer durationMonths; + + @NotNull + @Min(0) + private Integer clawbackMonths; + + @NotNull + private LocalDate effectiveFrom; + + private LocalDate effectiveTo; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportRuleSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportRuleSearchParam.java new file mode 100644 index 0000000..db0e89d --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportRuleSearchParam.java @@ -0,0 +1,14 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * settling_support_rule 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SettlingSupportRuleSearchParam extends SearchParam { + private String agentGrade; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportRuleVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportRuleVO.java new file mode 100644 index 0000000..98174a3 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/SettlingSupportRuleVO.java @@ -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; + +/** + * settling_support_rule — 정착지원금 룰 마스터 (V90) + */ +@Data +public class SettlingSupportRuleVO { + private Long ruleId; + /** 설계사 등급 */ + private String agentGrade; + /** 월 정액 지원금 */ + private BigDecimal supportAmount; + /** 약정 지원 개월수 */ + private Integer durationMonths; + /** 이 개월 내 해촉 시 환수 대상 */ + private Integer clawbackMonths; + private LocalDate effectiveFrom; + private LocalDate effectiveTo; + private LocalDateTime createdAt; + private Long createdBy; + private LocalDateTime updatedAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/StepCloseResp.java b/ga-core/src/main/java/com/ga/core/vo/commission/StepCloseResp.java new file mode 100644 index 0000000..cb929b2 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/StepCloseResp.java @@ -0,0 +1,22 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * settlement_step_close 조회 응답 — 코드명 포함. + */ +@Data +public class StepCloseResp { + private Long stepCloseId; + private String settleMonth; + private String stepCode; + private String stepCodeName; + private Integer stepSeq; + private String status; + private String statusName; + private LocalDateTime closedAt; + private Long closedBy; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/StepCloseSaveReq.java b/ga-core/src/main/java/com/ga/core/vo/commission/StepCloseSaveReq.java new file mode 100644 index 0000000..cef60ec --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/StepCloseSaveReq.java @@ -0,0 +1,26 @@ +package com.ga.core.vo.commission; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.Data; + +/** + * settlement_step_close 저장 요청 (INSERT 용). + * status/closedAt/closedBy 는 서버에서 결정. + */ +@Data +public class StepCloseSaveReq { + + @NotBlank + private String settleMonth; + + @NotBlank + private String stepCode; + + @NotNull + @Min(1) + @Max(4) + private Integer stepSeq; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/StepCloseSearchParam.java b/ga-core/src/main/java/com/ga/core/vo/commission/StepCloseSearchParam.java new file mode 100644 index 0000000..833e5a6 --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/StepCloseSearchParam.java @@ -0,0 +1,15 @@ +package com.ga.core.vo.commission; + +import com.ga.common.model.SearchParam; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * settlement_step_close 검색 조건. + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class StepCloseSearchParam extends SearchParam { + private String settleMonth; + private String stepCode; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/StepCloseVO.java b/ga-core/src/main/java/com/ga/core/vo/commission/StepCloseVO.java new file mode 100644 index 0000000..0b20f4c --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/StepCloseVO.java @@ -0,0 +1,25 @@ +package com.ga.core.vo.commission; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * settlement_step_close — 단계마감 (V97) + * 월 정산을 단계별(수신→계산→검증→지급)로 마감하는 상태 관리 테이블. + */ +@Data +public class StepCloseVO { + private Long stepCloseId; + /** 정산 귀속월 (YYYYMM) */ + private String settleMonth; + /** 단계코드 (RECEIVE/CALC/VERIFY/PAY) */ + private String stepCode; + /** 단계 순서 1~4 */ + private Integer stepSeq; + /** 상태 (OPEN=열림/CLOSED=마감) */ + private String status; + private LocalDateTime closedAt; + private Long closedBy; + private LocalDateTime createdAt; +} diff --git a/ga-core/src/main/java/com/ga/core/vo/commission/StepCode.java b/ga-core/src/main/java/com/ga/core/vo/commission/StepCode.java new file mode 100644 index 0000000..fb1c9fa --- /dev/null +++ b/ga-core/src/main/java/com/ga/core/vo/commission/StepCode.java @@ -0,0 +1,30 @@ +package com.ga.core.vo.commission; + +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * 단계마감 단계코드 (STEP_CODE 그룹, V97) + * settlement_step_close.step_code 에서 사용. + * 순서: RECEIVE(1) → CALC(2) → VERIFY(3) → PAY(4) + */ +public enum StepCode { + RECEIVE("수신마감"), + CALC("계산마감"), + VERIFY("검증마감"), + PAY("지급마감"); + + private final String label; + + StepCode(String label) { + this.label = label; + } + + @JsonValue + public String getCode() { + return this.name(); + } + + public String getLabel() { + return label; + } +} diff --git a/ga-core/src/main/java/com/ga/core/vo/settle/SettleMasterVO.java b/ga-core/src/main/java/com/ga/core/vo/settle/SettleMasterVO.java index eed2ba9..5349d99 100644 --- a/ga-core/src/main/java/com/ga/core/vo/settle/SettleMasterVO.java +++ b/ga-core/src/main/java/com/ga/core/vo/settle/SettleMasterVO.java @@ -16,6 +16,7 @@ public class SettleMasterVO { private BigDecimal exceptionMinus; private BigDecimal overrideTotal; private BigDecimal chargebackTotal; + private BigDecimal otherCommissionTotal; private BigDecimal grossAmount; private BigDecimal taxAmount; private BigDecimal netAmount; diff --git a/ga-core/src/main/resources/mapper/commission/AgentGradeEvaluationMapper.xml b/ga-core/src/main/resources/mapper/commission/AgentGradeEvaluationMapper.xml new file mode 100644 index 0000000..9733da2 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/AgentGradeEvaluationMapper.xml @@ -0,0 +1,82 @@ + + + + + + + + + + e.eval_id, + e.agent_id, + a.agent_name, + o.org_name, + e.eval_period, + e.performance_score, + e.prev_grade_id, + pg.grade_name AS prev_grade_name, + e.evaluated_grade_id, + eg.grade_name AS evaluated_grade_name, + e.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'GRADE_EVAL_STATUS' AND cc.code = e.status) AS status_name, + e.created_at + + + + + + + + + + INSERT INTO agent_grade_evaluation + (agent_id, eval_period, performance_score, prev_grade_id, evaluated_grade_id, status) + VALUES + (#{agentId}, #{evalPeriod}, #{performanceScore}, + #{prevGradeId}, #{evaluatedGradeId}, + COALESCE(#{status}, 'EVALUATED')) + + + + + UPDATE agent_grade_evaluation + SET status = #{status} + WHERE eval_id = #{evalId} + + + + DELETE FROM agent_grade_evaluation WHERE eval_id = #{evalId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/AgentGuaranteeInsuranceMapper.xml b/ga-core/src/main/resources/mapper/commission/AgentGuaranteeInsuranceMapper.xml new file mode 100644 index 0000000..ee14355 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/AgentGuaranteeInsuranceMapper.xml @@ -0,0 +1,83 @@ + + + + + + + + + + gi.guarantee_id, + gi.agent_id, + a.agent_name, + o.org_name, + gi.policy_no, + gi.carrier_id, + ic.company_name AS carrier_name, + gi.guarantee_amount, + gi.start_date, + gi.end_date, + gi.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'GUARANTEE_STATUS' AND cc.code = gi.status) AS status_name, + gi.created_at, + gi.updated_at + + + + + + + + + + INSERT INTO agent_guarantee_insurance + (agent_id, policy_no, carrier_id, guarantee_amount, start_date, end_date, status) + VALUES + (#{agentId}, #{policyNo}, #{carrierId}, #{guaranteeAmount}, + #{startDate}, #{endDate}, + COALESCE(#{status}, 'ACTIVE')) + + + + UPDATE agent_guarantee_insurance + SET policy_no = #{policyNo}, + carrier_id = #{carrierId}, + guarantee_amount = #{guaranteeAmount}, + start_date = #{startDate}, + end_date = #{endDate}, + status = #{status}, + updated_at = NOW() + WHERE guarantee_id = #{guaranteeId} + + + + DELETE FROM agent_guarantee_insurance WHERE guarantee_id = #{guaranteeId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/AgentReserveFundMapper.xml b/ga-core/src/main/resources/mapper/commission/AgentReserveFundMapper.xml new file mode 100644 index 0000000..8af3507 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/AgentReserveFundMapper.xml @@ -0,0 +1,108 @@ + + + + + + + + + + rf.reserve_id, + rf.agent_id, + a.agent_name, + o.org_name, + rf.settle_month, + rf.reserve_in, + rf.reserve_out, + rf.balance, + rf.created_at + + + + + rf.reserve_id, + rf.agent_id, + rf.settle_month, + rf.reserve_in, + rf.reserve_out, + rf.balance, + rf.created_at + + + + + + + + + + + + + + + + INSERT INTO agent_reserve_fund + (agent_id, settle_month, reserve_in, reserve_out, balance) + VALUES + (#{agentId}, #{settleMonth}, #{reserveIn}, #{reserveOut}, #{balance}) + + + + UPDATE agent_reserve_fund + SET reserve_in = #{reserveIn}, + reserve_out = #{reserveOut}, + balance = #{balance} + WHERE reserve_id = #{reserveId} + + + + DELETE FROM agent_reserve_fund WHERE reserve_id = #{reserveId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/AwardLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/AwardLedgerMapper.xml new file mode 100644 index 0000000..27f5fff --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/AwardLedgerMapper.xml @@ -0,0 +1,86 @@ + + + + + + + + + + l.ledger_id, + l.agent_id, a.agent_name, o.org_name, + l.settle_month, + l.award_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'AWARD_TYPE' AND cc.code = l.award_type) AS award_type_name, + l.base_amount, l.award_amount, + l.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'INCOME_STATUS' AND cc.code = l.status) AS status_name + + + + FROM award_ledger l + LEFT JOIN agent a ON a.agent_id = l.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + + + + + + + + + + + + INSERT INTO award_ledger + (agent_id, settle_month, award_type, base_amount, award_amount, status) + VALUES + (#{agentId}, #{settleMonth}, #{awardType}, #{baseAmount}, #{awardAmount}, + COALESCE(#{status}, 'CALCULATED')) + + + + UPDATE award_ledger + SET status = #{status}, updated_at = NOW() + WHERE ledger_id = #{ledgerId} + + + + DELETE FROM award_ledger WHERE ledger_id = #{ledgerId} + + + + + + diff --git a/ga-core/src/main/resources/mapper/commission/AwardRuleMapper.xml b/ga-core/src/main/resources/mapper/commission/AwardRuleMapper.xml new file mode 100644 index 0000000..846f60b --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/AwardRuleMapper.xml @@ -0,0 +1,75 @@ + + + + + + + + + + r.rule_id, + r.award_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'AWARD_TYPE' AND cc.code = r.award_type) AS award_type_name, + r.calc_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'CALC_TYPE' AND cc.code = r.calc_type) AS calc_type_name, + r.threshold_amount, r.award_rate, r.award_fixed, + r.effective_from, r.effective_to + + + + + + + + + + + INSERT INTO award_rule + (award_type, calc_type, threshold_amount, award_rate, award_fixed, + effective_from, effective_to, created_by) + VALUES + (#{awardType}, #{calcType}, #{thresholdAmount}, #{awardRate}, #{awardFixed}, + #{effectiveFrom}, #{effectiveTo}, #{createdBy}) + + + + UPDATE award_rule + SET calc_type = #{calcType}, + threshold_amount = #{thresholdAmount}, + award_rate = #{awardRate}, + award_fixed = #{awardFixed}, + effective_to = #{effectiveTo}, + updated_at = NOW() + WHERE rule_id = #{ruleId} + + + + DELETE FROM award_rule WHERE rule_id = #{ruleId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/BranchMgrAllowanceLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/BranchMgrAllowanceLedgerMapper.xml new file mode 100644 index 0000000..9fb0d7e --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/BranchMgrAllowanceLedgerMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + l.ledger_id, + l.manager_agent_id, a.agent_name AS manager_agent_name, + l.org_id, o.org_name, + l.settle_month, + l.base_amount, l.allowance_rate, l.allowance_amount, + l.clawback_amount, + l.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'SUPPORT_STATUS' AND cc.code = l.status) AS status_name + + + + FROM branch_mgr_allowance_ledger l + LEFT JOIN agent a ON a.agent_id = l.manager_agent_id + LEFT JOIN organization o ON o.org_id = l.org_id + + + + + + + + + + + + INSERT INTO branch_mgr_allowance_ledger + (manager_agent_id, org_id, settle_month, base_amount, + allowance_rate, allowance_amount, clawback_amount, status) + VALUES + (#{managerAgentId}, #{orgId}, #{settleMonth}, #{baseAmount}, + #{allowanceRate}, #{allowanceAmount}, COALESCE(#{clawbackAmount}, 0), + COALESCE(#{status}, 'CALCULATED')) + + + + UPDATE branch_mgr_allowance_ledger + SET status = #{status}, updated_at = NOW() + WHERE ledger_id = #{ledgerId} + + + + DELETE FROM branch_mgr_allowance_ledger WHERE ledger_id = #{ledgerId} + + + + + + diff --git a/ga-core/src/main/resources/mapper/commission/BranchMgrAllowanceRuleMapper.xml b/ga-core/src/main/resources/mapper/commission/BranchMgrAllowanceRuleMapper.xml new file mode 100644 index 0000000..5e16da9 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/BranchMgrAllowanceRuleMapper.xml @@ -0,0 +1,68 @@ + + + + + + + + + + r.rule_id, + r.position_code, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'POSITION_CODE' AND cc.code = r.position_code) AS position_code_name, + r.allowance_rate, r.cap_amount, + r.effective_from, r.effective_to + + + + + + + + + + + INSERT INTO branch_mgr_allowance_rule + (position_code, allowance_rate, cap_amount, + effective_from, effective_to, created_by) + VALUES + (#{positionCode}, #{allowanceRate}, #{capAmount}, + #{effectiveFrom}, #{effectiveTo}, #{createdBy}) + + + + UPDATE branch_mgr_allowance_rule + SET allowance_rate = #{allowanceRate}, + cap_amount = #{capAmount}, + effective_to = #{effectiveTo}, + updated_at = NOW() + WHERE rule_id = #{ruleId} + + + + DELETE FROM branch_mgr_allowance_rule WHERE rule_id = #{ruleId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/CollectionCommissionLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/CollectionCommissionLedgerMapper.xml index e568841..e56747a 100644 --- a/ga-core/src/main/resources/mapper/commission/CollectionCommissionLedgerMapper.xml +++ b/ga-core/src/main/resources/mapper/commission/CollectionCommissionLedgerMapper.xml @@ -75,4 +75,14 @@ DELETE FROM collection_commission_ledger WHERE ledger_id = #{ledgerId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/CommissionSimulationMapper.xml b/ga-core/src/main/resources/mapper/commission/CommissionSimulationMapper.xml new file mode 100644 index 0000000..ddd5b49 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/CommissionSimulationMapper.xml @@ -0,0 +1,72 @@ + + + + + + + + + + s.sim_id, + s.sim_name, + s.premium_amount, + s.company_rate, + s.payout_rate, + s.commission_year, + s.company_payment, + s.agent_commission, + s.withholding_tax, + s.net_amount, + s.expected_chargeback, + s.created_at, + s.created_by + + + + + + + + + + INSERT INTO commission_simulation + (sim_name, premium_amount, company_rate, payout_rate, commission_year, + company_payment, agent_commission, withholding_tax, net_amount, + expected_chargeback, created_by) + VALUES + (#{simName}, #{premiumAmount}, #{companyRate}, #{payoutRate}, #{commissionYear}, + #{companyPayment}, #{agentCommission}, #{withholdingTax}, #{netAmount}, + #{expectedChargeback}, #{createdBy}) + + + + DELETE FROM commission_simulation WHERE sim_id = #{simId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/GradeEvalRuleMapper.xml b/ga-core/src/main/resources/mapper/commission/GradeEvalRuleMapper.xml new file mode 100644 index 0000000..2060655 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/GradeEvalRuleMapper.xml @@ -0,0 +1,83 @@ + + + + + + + + + + r.rule_id, + r.grade_id, + g.grade_name, + r.min_score, + r.effective_from, + r.effective_to, + r.created_at + + + + + + + + + + + + INSERT INTO grade_eval_rule + (grade_id, min_score, effective_from, effective_to) + VALUES + (#{gradeId}, #{minScore}, #{effectiveFrom}, #{effectiveTo}) + + + + UPDATE grade_eval_rule + SET grade_id = #{gradeId}, + min_score = #{minScore}, + effective_from = #{effectiveFrom}, + effective_to = #{effectiveTo} + WHERE rule_id = #{ruleId} + + + + DELETE FROM grade_eval_rule WHERE rule_id = #{ruleId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/HqSettleItemMapper.xml b/ga-core/src/main/resources/mapper/commission/HqSettleItemMapper.xml new file mode 100644 index 0000000..cfd0c36 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/HqSettleItemMapper.xml @@ -0,0 +1,84 @@ + + + + + + + + + + i.item_id, + i.settle_month, + i.item_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'HQ_ITEM_TYPE' AND cc.code = i.item_type) AS item_type_name, + i.item_name, + i.amount, + i.memo, + i.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'MASTER_STATUS' AND cc.code = i.status) AS status_name, + i.created_at, + i.created_by, + i.updated_at + + + + + + + + + + + + + INSERT INTO hq_settle_item + (settle_month, item_type, item_name, amount, memo, status, created_by) + VALUES + (#{settleMonth}, #{itemType}, #{itemName}, #{amount}, #{memo}, + COALESCE(#{status}, 'REGISTERED'), #{createdBy}) + + + + UPDATE hq_settle_item + SET item_type = #{itemType}, + item_name = #{itemName}, + amount = #{amount}, + memo = #{memo}, + status = #{status}, + updated_at = NOW() + WHERE item_id = #{itemId} + + + + DELETE FROM hq_settle_item WHERE item_id = #{itemId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/IncomeCommissionLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/IncomeCommissionLedgerMapper.xml new file mode 100644 index 0000000..edf4596 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/IncomeCommissionLedgerMapper.xml @@ -0,0 +1,78 @@ + + + + + + + + + + l.income_id, l.contract_id, ct.policy_no, + l.carrier_id, ic.company_name AS carrier_name, + l.settle_month, + l.premium_amount, l.company_rate, l.income_amount, + l.commission_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'COMMISSION_TYPE' AND cc.code = l.commission_type) AS commission_type_name, + l.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'INCOME_STATUS' AND cc.code = l.status) AS status_name + + + + FROM income_commission_ledger l + LEFT JOIN insurance_company ic ON ic.company_id = l.carrier_id + LEFT JOIN contract ct ON ct.contract_id = l.contract_id + + + + + + + + + + + + INSERT INTO income_commission_ledger + (contract_id, carrier_id, settle_month, premium_amount, company_rate, + income_amount, commission_type, status) + VALUES + (#{contractId}, #{carrierId}, #{settleMonth}, #{premiumAmount}, #{companyRate}, + #{incomeAmount}, #{commissionType}, + COALESCE(#{status}, 'CALCULATED')) + + + + UPDATE income_commission_ledger + SET status = #{status}, updated_at = NOW() + WHERE income_id = #{incomeId} + + + + DELETE FROM income_commission_ledger WHERE income_id = #{incomeId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/JointContractMapper.xml b/ga-core/src/main/resources/mapper/commission/JointContractMapper.xml new file mode 100644 index 0000000..0c8361f --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/JointContractMapper.xml @@ -0,0 +1,94 @@ + + + + + + + + + + jc.joint_id, + jc.contract_id, + ct.policy_no, + jc.agent_id, + a.agent_name, + o.org_name, + jc.role, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'JOINT_ROLE' AND cc.code = jc.role) AS role_name, + jc.share_rate, + jc.created_at, + jc.created_by + + + + + + + + + + + + + + + INSERT INTO joint_contract + (contract_id, agent_id, role, share_rate, created_by) + VALUES + (#{contractId}, #{agentId}, #{role}, #{shareRate}, #{createdBy}) + + + + UPDATE joint_contract + SET role = #{role}, + share_rate = #{shareRate} + WHERE joint_id = #{jointId} + + + + DELETE FROM joint_contract WHERE joint_id = #{jointId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/LifeSupportLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/LifeSupportLedgerMapper.xml new file mode 100644 index 0000000..6a38710 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/LifeSupportLedgerMapper.xml @@ -0,0 +1,85 @@ + + + + + + + + + + l.ledger_id, + l.agent_id, a.agent_name, o.org_name, + l.settle_month, + l.base_amount, l.support_rate, l.support_amount, + l.clawback_amount, l.net_amount, + l.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'SUPPORT_STATUS' AND cc.code = l.status) AS status_name + + + + FROM life_support_ledger l + LEFT JOIN agent a ON a.agent_id = l.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + + + + + + + + + + + + INSERT INTO life_support_ledger + (agent_id, settle_month, base_amount, support_rate, + support_amount, clawback_amount, net_amount, status) + VALUES + (#{agentId}, #{settleMonth}, #{baseAmount}, #{supportRate}, + #{supportAmount}, COALESCE(#{clawbackAmount}, 0), #{netAmount}, + COALESCE(#{status}, 'CALCULATED')) + + + + UPDATE life_support_ledger + SET status = #{status}, updated_at = NOW() + WHERE ledger_id = #{ledgerId} + + + + DELETE FROM life_support_ledger WHERE ledger_id = #{ledgerId} + + + + + + diff --git a/ga-core/src/main/resources/mapper/commission/LifeSupportRuleMapper.xml b/ga-core/src/main/resources/mapper/commission/LifeSupportRuleMapper.xml new file mode 100644 index 0000000..3c215bd --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/LifeSupportRuleMapper.xml @@ -0,0 +1,65 @@ + + + + + + + + + + r.rule_id, + r.agent_grade, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'AGENT_GRADE' AND cc.code = r.agent_grade) AS agent_grade_name, + r.support_rate, + r.effective_from, r.effective_to + + + + + + + + + + + INSERT INTO life_support_rule + (agent_grade, support_rate, effective_from, effective_to, created_by) + VALUES + (#{agentGrade}, #{supportRate}, #{effectiveFrom}, #{effectiveTo}, #{createdBy}) + + + + UPDATE life_support_rule + SET support_rate = #{supportRate}, + effective_to = #{effectiveTo}, + updated_at = NOW() + WHERE rule_id = #{ruleId} + + + + DELETE FROM life_support_rule WHERE rule_id = #{ruleId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/MarginSummaryMapper.xml b/ga-core/src/main/resources/mapper/commission/MarginSummaryMapper.xml new file mode 100644 index 0000000..d8d9923 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/MarginSummaryMapper.xml @@ -0,0 +1,87 @@ + + + + + + + + + + m.margin_id, m.settle_month, + m.agent_id, a.agent_name, + m.income_total, m.payout_total, m.margin_amount, m.margin_rate + + + + FROM margin_summary m + LEFT JOIN agent a ON a.agent_id = m.agent_id + + + + + + + + + + + + INSERT INTO margin_summary + (settle_month, agent_id, income_total, payout_total, margin_amount, margin_rate) + VALUES + (#{settleMonth}, #{agentId}, #{incomeTotal}, #{payoutTotal}, #{marginAmount}, #{marginRate}) + + + + UPDATE margin_summary + SET income_total = #{incomeTotal}, + payout_total = #{payoutTotal}, + margin_amount = #{marginAmount}, + margin_rate = #{marginRate} + WHERE margin_id = #{marginId} + + + + DELETE FROM margin_summary WHERE margin_id = #{marginId} + + + + + + + + + diff --git a/ga-core/src/main/resources/mapper/commission/PersistencyBonusMapper.xml b/ga-core/src/main/resources/mapper/commission/PersistencyBonusMapper.xml index 3449ae1..3a67525 100644 --- a/ga-core/src/main/resources/mapper/commission/PersistencyBonusMapper.xml +++ b/ga-core/src/main/resources/mapper/commission/PersistencyBonusMapper.xml @@ -76,4 +76,14 @@ DELETE FROM persistency_bonus WHERE bonus_id = #{bonusId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/ProductConversionRateMapper.xml b/ga-core/src/main/resources/mapper/commission/ProductConversionRateMapper.xml new file mode 100644 index 0000000..ca7661f --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/ProductConversionRateMapper.xml @@ -0,0 +1,64 @@ + + + + + + + + + + r.rate_id, r.product_id, p.product_name, + r.conversion_rate, + r.effective_from, r.effective_to + + + + + + + + + + + INSERT INTO product_conversion_rate + (product_id, conversion_rate, effective_from, effective_to, created_by) + VALUES + (#{productId}, #{conversionRate}, #{effectiveFrom}, #{effectiveTo}, #{createdBy}) + + + + UPDATE product_conversion_rate + SET conversion_rate = #{conversionRate}, + effective_to = #{effectiveTo}, + updated_at = NOW() + WHERE rate_id = #{rateId} + + + + DELETE FROM product_conversion_rate WHERE rate_id = #{rateId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/RecognizedPerformanceLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/RecognizedPerformanceLedgerMapper.xml new file mode 100644 index 0000000..edf055d --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/RecognizedPerformanceLedgerMapper.xml @@ -0,0 +1,78 @@ + + + + + + + + + + l.ledger_id, + l.agent_id, a.agent_name, o.org_name, + l.contract_id, ct.policy_no, + l.settle_month, + l.premium_amount, l.conversion_rate, l.converted_premium, + l.recognition_rate, l.recognized_amount, + l.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'INCOME_STATUS' AND cc.code = l.status) AS status_name + + + + FROM recognized_performance_ledger l + LEFT JOIN agent a ON a.agent_id = l.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + LEFT JOIN contract ct ON ct.contract_id = l.contract_id + + + + + + + + + + + + INSERT INTO recognized_performance_ledger + (agent_id, contract_id, settle_month, premium_amount, conversion_rate, + converted_premium, recognition_rate, recognized_amount, status) + VALUES + (#{agentId}, #{contractId}, #{settleMonth}, #{premiumAmount}, #{conversionRate}, + #{convertedPremium}, #{recognitionRate}, #{recognizedAmount}, + COALESCE(#{status}, 'CALCULATED')) + + + + UPDATE recognized_performance_ledger + SET status = #{status}, updated_at = NOW() + WHERE ledger_id = #{ledgerId} + + + + DELETE FROM recognized_performance_ledger WHERE ledger_id = #{ledgerId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/ReferralCommissionLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/ReferralCommissionLedgerMapper.xml new file mode 100644 index 0000000..5ee1c7a --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/ReferralCommissionLedgerMapper.xml @@ -0,0 +1,88 @@ + + + + + + + + + + l.ledger_id, l.contract_id, ct.policy_no, + l.referrer_agent_id, a.agent_name AS referrer_agent_name, + l.settle_month, + l.referral_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'REFERRAL_TYPE' AND cc.code = l.referral_type) AS referral_type_name, + l.base_amount, l.commission_rate, l.commission_amount, + l.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'INCOME_STATUS' AND cc.code = l.status) AS status_name + + + + FROM referral_commission_ledger l + LEFT JOIN agent a ON a.agent_id = l.referrer_agent_id + LEFT JOIN contract ct ON ct.contract_id = l.contract_id + + + + + + + + + + + + INSERT INTO referral_commission_ledger + (contract_id, referrer_agent_id, settle_month, referral_type, + base_amount, commission_rate, commission_amount, status) + VALUES + (#{contractId}, #{referrerAgentId}, #{settleMonth}, #{referralType}, + #{baseAmount}, #{commissionRate}, #{commissionAmount}, + COALESCE(#{status}, 'CALCULATED')) + + + + UPDATE referral_commission_ledger + SET status = #{status}, updated_at = NOW() + WHERE ledger_id = #{ledgerId} + + + + DELETE FROM referral_commission_ledger WHERE ledger_id = #{ledgerId} + + + + + + diff --git a/ga-core/src/main/resources/mapper/commission/ReferralCommissionRuleMapper.xml b/ga-core/src/main/resources/mapper/commission/ReferralCommissionRuleMapper.xml new file mode 100644 index 0000000..f631fe0 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/ReferralCommissionRuleMapper.xml @@ -0,0 +1,73 @@ + + + + + + + + + + r.rule_id, + r.referral_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'REFERRAL_TYPE' AND cc.code = r.referral_type) AS referral_type_name, + r.calc_type, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'CALC_TYPE' AND cc.code = r.calc_type) AS calc_type_name, + r.commission_rate, r.fixed_amount, + r.effective_from, r.effective_to + + + + + + + + + + + INSERT INTO referral_commission_rule + (referral_type, calc_type, commission_rate, fixed_amount, + effective_from, effective_to, created_by) + VALUES + (#{referralType}, #{calcType}, #{commissionRate}, #{fixedAmount}, + #{effectiveFrom}, #{effectiveTo}, #{createdBy}) + + + + UPDATE referral_commission_rule + SET calc_type = #{calcType}, + commission_rate = #{commissionRate}, + fixed_amount = #{fixedAmount}, + effective_to = #{effectiveTo}, + updated_at = NOW() + WHERE rule_id = #{ruleId} + + + + DELETE FROM referral_commission_rule WHERE rule_id = #{ruleId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/ReinstatementCommissionLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/ReinstatementCommissionLedgerMapper.xml index 154d645..e0db921 100644 --- a/ga-core/src/main/resources/mapper/commission/ReinstatementCommissionLedgerMapper.xml +++ b/ga-core/src/main/resources/mapper/commission/ReinstatementCommissionLedgerMapper.xml @@ -70,4 +70,14 @@ DELETE FROM reinstatement_commission_ledger WHERE ledger_id = #{ledgerId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/RenewalCommissionLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/RenewalCommissionLedgerMapper.xml index b6711be..58362a0 100644 --- a/ga-core/src/main/resources/mapper/commission/RenewalCommissionLedgerMapper.xml +++ b/ga-core/src/main/resources/mapper/commission/RenewalCommissionLedgerMapper.xml @@ -68,4 +68,14 @@ DELETE FROM renewal_commission_ledger WHERE ledger_id = #{ledgerId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/SettlingSupportLedgerMapper.xml b/ga-core/src/main/resources/mapper/commission/SettlingSupportLedgerMapper.xml new file mode 100644 index 0000000..6400780 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/SettlingSupportLedgerMapper.xml @@ -0,0 +1,82 @@ + + + + + + + + + + l.ledger_id, l.agent_id, a.agent_name, o.org_name, + l.settle_month, + l.support_amount, l.clawback_amount, l.net_amount, + l.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'SUPPORT_STATUS' AND cc.code = l.status) AS status_name + + + + FROM settling_support_ledger l + LEFT JOIN agent a ON a.agent_id = l.agent_id + LEFT JOIN organization o ON o.org_id = a.org_id + + + + + + + + + + + + INSERT INTO settling_support_ledger + (agent_id, settle_month, support_amount, clawback_amount, net_amount, status) + VALUES + (#{agentId}, #{settleMonth}, #{supportAmount}, + COALESCE(#{clawbackAmount}, 0), #{netAmount}, + COALESCE(#{status}, 'CALCULATED')) + + + + UPDATE settling_support_ledger + SET status = #{status}, updated_at = NOW() + WHERE ledger_id = #{ledgerId} + + + + DELETE FROM settling_support_ledger WHERE ledger_id = #{ledgerId} + + + + + + diff --git a/ga-core/src/main/resources/mapper/commission/SettlingSupportRuleMapper.xml b/ga-core/src/main/resources/mapper/commission/SettlingSupportRuleMapper.xml new file mode 100644 index 0000000..aa90cf9 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/SettlingSupportRuleMapper.xml @@ -0,0 +1,69 @@ + + + + + + + + + + r.rule_id, + r.agent_grade, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'AGENT_GRADE' AND cc.code = r.agent_grade) AS agent_grade_name, + r.support_amount, r.duration_months, r.clawback_months, + r.effective_from, r.effective_to + + + + + + + + + + + INSERT INTO settling_support_rule + (agent_grade, support_amount, duration_months, clawback_months, + effective_from, effective_to, created_by) + VALUES + (#{agentGrade}, #{supportAmount}, #{durationMonths}, #{clawbackMonths}, + #{effectiveFrom}, #{effectiveTo}, #{createdBy}) + + + + UPDATE settling_support_rule + SET support_amount = #{supportAmount}, + duration_months = #{durationMonths}, + clawback_months = #{clawbackMonths}, + effective_to = #{effectiveTo}, + updated_at = NOW() + WHERE rule_id = #{ruleId} + + + + DELETE FROM settling_support_rule WHERE rule_id = #{ruleId} + + + diff --git a/ga-core/src/main/resources/mapper/commission/StepCloseMapper.xml b/ga-core/src/main/resources/mapper/commission/StepCloseMapper.xml new file mode 100644 index 0000000..09fff90 --- /dev/null +++ b/ga-core/src/main/resources/mapper/commission/StepCloseMapper.xml @@ -0,0 +1,98 @@ + + + + + + + + + + + + sc.step_close_id, + sc.settle_month, + sc.step_code, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'STEP_CODE' AND cc.code = sc.step_code) AS step_code_name, + sc.step_seq, + sc.status, + (SELECT cc.code_name FROM common_code cc + WHERE cc.group_code = 'STEP_STATUS' AND cc.code = sc.status) AS status_name, + sc.closed_at, + sc.closed_by, + sc.created_at + + + + + + + + + + + + + + INSERT INTO settlement_step_close + (settle_month, step_code, step_seq, status) + VALUES + (#{settleMonth}, #{stepCode}, #{stepSeq}, + COALESCE(#{status}, 'OPEN')) + + + + + UPDATE settlement_step_close + SET status = #{status}, + closed_at = + + NOW() + NULL + , + closed_by = + + #{closedBy} + NULL + + WHERE step_close_id = #{stepCloseId} + + + + DELETE FROM settlement_step_close WHERE step_close_id = #{stepCloseId} + + + diff --git a/ga-core/src/main/resources/mapper/settle/SettleMasterMapper.xml b/ga-core/src/main/resources/mapper/settle/SettleMasterMapper.xml index 6483440..5eb5e11 100644 --- a/ga-core/src/main/resources/mapper/settle/SettleMasterMapper.xml +++ b/ga-core/src/main/resources/mapper/settle/SettleMasterMapper.xml @@ -71,25 +71,28 @@ INSERT INTO settle_master (agent_id, settle_month, recruit_total, maintain_total, exception_plus, exception_minus, override_total, chargeback_total, + other_commission_total, gross_amount, tax_amount, net_amount, status, calc_date) VALUES (#{agentId}, #{settleMonth}, COALESCE(#{recruitTotal}, 0), COALESCE(#{maintainTotal}, 0), COALESCE(#{exceptionPlus}, 0), COALESCE(#{exceptionMinus}, 0), COALESCE(#{overrideTotal}, 0), COALESCE(#{chargebackTotal}, 0), + COALESCE(#{otherCommissionTotal}, 0), COALESCE(#{grossAmount}, 0), COALESCE(#{taxAmount}, 0), COALESCE(#{netAmount}, 0), COALESCE(#{status},'CALCULATED'), NOW()) ON CONFLICT (agent_id, settle_month) DO UPDATE SET - recruit_total = EXCLUDED.recruit_total, - maintain_total = EXCLUDED.maintain_total, - exception_plus = EXCLUDED.exception_plus, - exception_minus = EXCLUDED.exception_minus, - override_total = EXCLUDED.override_total, - chargeback_total = EXCLUDED.chargeback_total, - gross_amount = EXCLUDED.gross_amount, - tax_amount = EXCLUDED.tax_amount, - net_amount = EXCLUDED.net_amount, - status = EXCLUDED.status, - calc_date = NOW() + recruit_total = EXCLUDED.recruit_total, + maintain_total = EXCLUDED.maintain_total, + exception_plus = EXCLUDED.exception_plus, + exception_minus = EXCLUDED.exception_minus, + override_total = EXCLUDED.override_total, + chargeback_total = EXCLUDED.chargeback_total, + other_commission_total = EXCLUDED.other_commission_total, + gross_amount = EXCLUDED.gross_amount, + tax_amount = EXCLUDED.tax_amount, + net_amount = EXCLUDED.net_amount, + status = EXCLUDED.status, + calc_date = NOW() WHERE settle_master.status NOT IN ('CONFIRMED','PAID') diff --git a/ga-frontend/src/App.tsx b/ga-frontend/src/App.tsx index ff3bd97..1c751da 100644 --- a/ga-frontend/src/App.tsx +++ b/ga-frontend/src/App.tsx @@ -1,101 +1,139 @@ +import React, { Suspense } from 'react'; import { Routes, Route, Navigate } from 'react-router-dom'; +import { Spin } from 'antd'; import LoginPage from '@/pages/LoginPage'; import MainLayout from '@/layouts/MainLayout'; -import Dashboard from '@/pages/Dashboard'; -// ── P5: 연말명세서 / 분개장 ────────────────────────── -import AgentAnnualIncomeList from '@/pages/income/AgentAnnualIncomeList'; -import AccountingJournalList from '@/pages/accounting/AccountingJournalList'; +// ── 페이지 레이지 로딩 ────────────────────────────────── +const Dashboard = React.lazy(() => import('@/pages/Dashboard')); -// ── P5-W2: 원천세 / 부가세 신고 ────────────────────── -import WithholdingTaxReportList from '@/pages/tax/WithholdingTaxReportList'; -import VatReportList from '@/pages/tax/VatReportList'; +// P5: 연말명세서 / 분개장 +const AgentAnnualIncomeList = React.lazy(() => import('@/pages/income/AgentAnnualIncomeList')); +const AccountingJournalList = React.lazy(() => import('@/pages/accounting/AccountingJournalList')); -// ── P5-W3: 결산 / 연말명세서 / 이상치 / 예측 ────────── -import AccountingCloseList from '@/pages/accounting/AccountingCloseList'; -import YearEndStatementList from '@/pages/income/YearEndStatementList'; -import RetentionAnomalyList from '@/pages/kpi/RetentionAnomalyList'; -import ForecastKpiList from '@/pages/kpi/ForecastKpiList'; +// P5-W2: 원천세 / 부가세 신고 +const WithholdingTaxReportList = React.lazy(() => import('@/pages/tax/WithholdingTaxReportList')); +const VatReportList = React.lazy(() => import('@/pages/tax/VatReportList')); -// ── P4: 수수료 종류 ────────────────────────────────── -import CommissionMaster from '@/pages/commission/CommissionMaster'; -import CollectionCommission from '@/pages/commission/CollectionCommission'; -import RenewalCommission from '@/pages/commission/RenewalCommission'; -import ReinstatementCommission from '@/pages/commission/ReinstatementCommission'; -import PersistencyBonus from '@/pages/commission/PersistencyBonus'; -import OverrideLedger from '@/pages/commission/OverrideLedger'; +// P5-W3: 결산 / 연말명세서 / 이상치 / 예측 +const AccountingCloseList = React.lazy(() => import('@/pages/accounting/AccountingCloseList')); +const YearEndStatementList = React.lazy(() => import('@/pages/income/YearEndStatementList')); +const RetentionAnomalyList = React.lazy(() => import('@/pages/kpi/RetentionAnomalyList')); +const ForecastKpiList = React.lazy(() => import('@/pages/kpi/ForecastKpiList')); -// ── P4: 설계사 라이프사이클 ─────────────────────────── -import AgentContracts from '@/pages/agent/AgentContracts'; -import AgentLicenses from '@/pages/agent/AgentLicenses'; -import AgentTrainings from '@/pages/agent/AgentTrainings'; +// P8: 운영 7도메인 +const StepClose = React.lazy(() => import('@/pages/settle/StepClose')); +const HqSettle = React.lazy(() => import('@/pages/settle/HqSettle')); +const GradeEvaluation = React.lazy(() => import('@/pages/org/GradeEvaluation')); +const JointContract = React.lazy(() => import('@/pages/contracts/JointContract')); +const GuaranteeInsurance = React.lazy(() => import('@/pages/org/GuaranteeInsurance')); +const ReserveFund = React.lazy(() => import('@/pages/commission/ReserveFund')); +const CommissionSimulator = React.lazy(() => import('@/pages/commission/CommissionSimulator')); -// ── P4: 계약 운영 ──────────────────────────────────── -import ContractEndorsements from '@/pages/contracts/ContractEndorsements'; -import ContractSuccessions from '@/pages/contracts/ContractSuccessions'; -import Complaints from '@/pages/contracts/Complaints'; +// P7: 수수료 계산 7도메인 +const IncomeCommission = React.lazy(() => import('@/pages/commission/IncomeCommission')); +const ReferralCommission = React.lazy(() => import('@/pages/commission/ReferralCommission')); +const SettlingSupport = React.lazy(() => import('@/pages/commission/SettlingSupport')); +const RecognizedPerformance = React.lazy(() => import('@/pages/commission/RecognizedPerformance')); +const Award = React.lazy(() => import('@/pages/commission/Award')); +const LifeSupport = React.lazy(() => import('@/pages/commission/LifeSupport')); +const BranchAllowance = React.lazy(() => import('@/pages/commission/BranchAllowance')); -// ── P4: 시스템 ────────────────────────────────────── -import Approvals from '@/pages/system/Approvals'; -import Notices from '@/pages/system/Notices'; +// P4: 수수료 종류 +const CommissionMaster = React.lazy(() => import('@/pages/commission/CommissionMaster')); +const CollectionCommission = React.lazy(() => import('@/pages/commission/CollectionCommission')); +const RenewalCommission = React.lazy(() => import('@/pages/commission/RenewalCommission')); +const ReinstatementCommission = React.lazy(() => import('@/pages/commission/ReinstatementCommission')); +const PersistencyBonus = React.lazy(() => import('@/pages/commission/PersistencyBonus')); +const OverrideLedger = React.lazy(() => import('@/pages/commission/OverrideLedger')); -// ── P4: 출금 ──────────────────────────────────────── -import WithdrawRequest from '@/pages/payments/WithdrawRequest'; +// P4: 설계사 라이프사이클 +const AgentContracts = React.lazy(() => import('@/pages/agent/AgentContracts')); +const AgentLicenses = React.lazy(() => import('@/pages/agent/AgentLicenses')); +const AgentTrainings = React.lazy(() => import('@/pages/agent/AgentTrainings')); -import OrgTree from '@/pages/org/OrgTree'; -import AgentList from '@/pages/org/AgentList'; -import AgentDetail from '@/pages/org/AgentDetail'; -import AgentForm from '@/pages/org/AgentForm'; +// P4: 계약 운영 +const ContractEndorsements = React.lazy(() => import('@/pages/contracts/ContractEndorsements')); +const ContractSuccessions = React.lazy(() => import('@/pages/contracts/ContractSuccessions')); +const Complaints = React.lazy(() => import('@/pages/contracts/Complaints')); -import CompanyList from '@/pages/product/CompanyList'; -import ProductList from '@/pages/product/ProductList'; -import ContractList from '@/pages/product/ContractList'; +// P4: 시스템 +const Approvals = React.lazy(() => import('@/pages/system/Approvals')); +const Notices = React.lazy(() => import('@/pages/system/Notices')); -import CommissionRateList from '@/pages/rule/CommissionRateList'; -import PayoutRuleList from '@/pages/rule/PayoutRuleList'; -import OverrideRuleList from '@/pages/rule/OverrideRuleList'; -import ChargebackRuleList from '@/pages/rule/ChargebackRuleList'; -import ExceptionCodeList from '@/pages/rule/ExceptionCodeList'; -import RegulatoryLimitList from '@/pages/rule/RegulatoryLimitList'; -import InstallmentRatioList from '@/pages/rule/InstallmentRatioList'; -import ChargebackGradeList from '@/pages/rule/ChargebackGradeList'; +// P4: 출금 +const WithdrawRequest = React.lazy(() => import('@/pages/payments/WithdrawRequest')); -import ReceiveData from '@/pages/receive/ReceiveData'; -import ReceiveMapping from '@/pages/receive/ReceiveMapping'; +// 조직 / 인사 +const OrgTree = React.lazy(() => import('@/pages/org/OrgTree')); +const AgentList = React.lazy(() => import('@/pages/org/AgentList')); +const AgentDetail = React.lazy(() => import('@/pages/org/AgentDetail')); +const AgentForm = React.lazy(() => import('@/pages/org/AgentForm')); -import RecruitLedger from '@/pages/ledger/RecruitLedger'; -import MaintainLedger from '@/pages/ledger/MaintainLedger'; -import ExceptionLedger from '@/pages/ledger/ExceptionLedger'; +// 상품 / 계약 +const CompanyList = React.lazy(() => import('@/pages/product/CompanyList')); +const ProductList = React.lazy(() => import('@/pages/product/ProductList')); +const ContractList = React.lazy(() => import('@/pages/product/ContractList')); -import SettleList from '@/pages/settle/SettleList'; -import BatchRun from '@/pages/settle/BatchRun'; -import PaymentList from '@/pages/settle/PaymentList'; -import DeductionList from '@/pages/settle/DeductionList'; -import PeriodClose from '@/pages/settle/PeriodClose'; -import SettleCorrection from '@/pages/settle/SettleCorrection'; -import TaxInvoiceList from '@/pages/settle/TaxInvoiceList'; -import CommissionDispute from '@/pages/settle/CommissionDispute'; -import IncentiveProgram from '@/pages/settle/IncentiveProgram'; +// 수수료 규정 +const CommissionRateList = React.lazy(() => import('@/pages/rule/CommissionRateList')); +const PayoutRuleList = React.lazy(() => import('@/pages/rule/PayoutRuleList')); +const OverrideRuleList = React.lazy(() => import('@/pages/rule/OverrideRuleList')); +const ChargebackRuleList = React.lazy(() => import('@/pages/rule/ChargebackRuleList')); +const ExceptionCodeList = React.lazy(() => import('@/pages/rule/ExceptionCodeList')); +const RegulatoryLimitList = React.lazy(() => import('@/pages/rule/RegulatoryLimitList')); +const InstallmentRatioList = React.lazy(() => import('@/pages/rule/InstallmentRatioList')); +const ChargebackGradeList = React.lazy(() => import('@/pages/rule/ChargebackGradeList')); -import PerformanceReport from '@/pages/report/PerformanceReport'; -import OrgReport from '@/pages/report/OrgReport'; -import ChargebackRiskReport from '@/pages/report/ChargebackRiskReport'; +// 데이터 수신 +const ReceiveData = React.lazy(() => import('@/pages/receive/ReceiveData')); +const ReceiveMapping = React.lazy(() => import('@/pages/receive/ReceiveMapping')); -import KpiMonthlySummary from '@/pages/kpi/KpiMonthlySummary'; -import KpiOrgSummary from '@/pages/kpi/KpiOrgSummary'; -import KpiRetention from '@/pages/kpi/KpiRetention'; -import KpiCumulative from '@/pages/kpi/KpiCumulative'; +// 원장 +const RecruitLedger = React.lazy(() => import('@/pages/ledger/RecruitLedger')); +const MaintainLedger = React.lazy(() => import('@/pages/ledger/MaintainLedger')); +const ExceptionLedger = React.lazy(() => import('@/pages/ledger/ExceptionLedger')); -import UserList from '@/pages/system/UserList'; -import RoleList from '@/pages/system/RoleList'; -import MenuManage from '@/pages/system/MenuManage'; -import CodeList from '@/pages/system/CodeList'; -import SystemConfig from '@/pages/system/SystemConfig'; -import SystemLog from '@/pages/system/SystemLog'; -import UploadTemplateManager from '@/pages/system/UploadTemplateManager'; -import DataDomainList from '@/pages/system/DataDomainList'; -import DataDictionary from '@/pages/system/DataDictionary'; -import ExternalCallLogList from '@/pages/system/ExternalCallLogList'; +// 정산 / 지급 +const SettleList = React.lazy(() => import('@/pages/settle/SettleList')); +const BatchRun = React.lazy(() => import('@/pages/settle/BatchRun')); +const PaymentList = React.lazy(() => import('@/pages/settle/PaymentList')); +const DeductionList = React.lazy(() => import('@/pages/settle/DeductionList')); +const PeriodClose = React.lazy(() => import('@/pages/settle/PeriodClose')); +const SettleCorrection = React.lazy(() => import('@/pages/settle/SettleCorrection')); +const TaxInvoiceList = React.lazy(() => import('@/pages/settle/TaxInvoiceList')); +const CommissionDispute = React.lazy(() => import('@/pages/settle/CommissionDispute')); +const IncentiveProgram = React.lazy(() => import('@/pages/settle/IncentiveProgram')); + +// 리포트 +const PerformanceReport = React.lazy(() => import('@/pages/report/PerformanceReport')); +const OrgReport = React.lazy(() => import('@/pages/report/OrgReport')); +const ChargebackRiskReport = React.lazy(() => import('@/pages/report/ChargebackRiskReport')); + +// KPI 대시보드 +const KpiMonthlySummary = React.lazy(() => import('@/pages/kpi/KpiMonthlySummary')); +const KpiOrgSummary = React.lazy(() => import('@/pages/kpi/KpiOrgSummary')); +const KpiRetention = React.lazy(() => import('@/pages/kpi/KpiRetention')); +const KpiCumulative = React.lazy(() => import('@/pages/kpi/KpiCumulative')); + +// 시스템관리 +const UserList = React.lazy(() => import('@/pages/system/UserList')); +const RoleList = React.lazy(() => import('@/pages/system/RoleList')); +const MenuManage = React.lazy(() => import('@/pages/system/MenuManage')); +const CodeList = React.lazy(() => import('@/pages/system/CodeList')); +const SystemConfig = React.lazy(() => import('@/pages/system/SystemConfig')); +const SystemLog = React.lazy(() => import('@/pages/system/SystemLog')); +const UploadTemplateManager = React.lazy(() => import('@/pages/system/UploadTemplateManager')); +const DataDomainList = React.lazy(() => import('@/pages/system/DataDomainList')); +const DataDictionary = React.lazy(() => import('@/pages/system/DataDictionary')); +const ExternalCallLogList = React.lazy(() => import('@/pages/system/ExternalCallLogList')); + +// ── 로딩 폴백 ──────────────────────────────────────────── +const PageFallback = ( +
+ +
+); function RequireAuth({ children }: { children: React.ReactNode }) { const token = localStorage.getItem('accessToken'); @@ -104,116 +142,136 @@ function RequireAuth({ children }: { children: React.ReactNode }) { export default function App() { return ( - - } /> - }> - } /> + + + } /> + }> + } /> - {/* 조직 / 인사 */} - } /> - } /> - } /> - } /> - } /> + {/* 조직 / 인사 */} + } /> + } /> + } /> + } /> + } /> - {/* 상품 / 계약 */} - } /> - } /> - } /> + {/* 상품 / 계약 */} + } /> + } /> + } /> - {/* 수수료 규정 */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + {/* 수수료 규정 */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> - {/* 데이터 수신 */} - } /> - } /> + {/* 데이터 수신 */} + } /> + } /> - {/* 원장 */} - } /> - } /> - } /> + {/* 원장 */} + } /> + } /> + } /> - {/* 정산 / 지급 */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + {/* 정산 / 지급 */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> - {/* 리포트 */} - } /> - } /> - } /> + {/* 리포트 */} + } /> + } /> + } /> - {/* KPI 대시보드 */} - } /> - } /> - } /> - } /> - } /> - } /> + {/* KPI 대시보드 */} + } /> + } /> + } /> + } /> + } /> + } /> - {/* P5: 연말명세서 / 분개장 */} - } /> - } /> + {/* P5: 연말명세서 / 분개장 */} + } /> + } /> - {/* P5-W2: 원천세 / 부가세 신고 */} - } /> - } /> + {/* P5-W2: 원천세 / 부가세 신고 */} + } /> + } /> - {/* P5-W3: 회계 결산 / 연말 지급명세서 */} - } /> - } /> + {/* P5-W3: 회계 결산 / 연말 지급명세서 */} + } /> + } /> - {/* P4: 수수료 종류 */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> + {/* P8: 운영 7도메인 */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> - {/* P4: 설계사 라이프사이클 */} - } /> - } /> - } /> + {/* P7: 수수료 계산 7도메인 */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> - {/* P4: 계약 운영 */} - } /> - } /> - } /> + {/* P4: 수수료 종류 */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> - {/* P4: 시스템 */} - } /> - } /> + {/* P4: 설계사 라이프사이클 */} + } /> + } /> + } /> - {/* P4: 출금 */} - } /> + {/* P4: 계약 운영 */} + } /> + } /> + } /> - {/* 시스템관리 */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - + {/* P4: 시스템 */} + } /> + } /> + + {/* P4: 출금 */} + } /> + + {/* 시스템관리 */} + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + ); } diff --git a/ga-frontend/src/api/commission.ts b/ga-frontend/src/api/commission.ts index 65e571f..9b9b9c9 100644 --- a/ga-frontend/src/api/commission.ts +++ b/ga-frontend/src/api/commission.ts @@ -144,6 +144,173 @@ export interface OverrideLedgerSearchParam { pageSize?: number; } +// ── P7: IncomeCommission ────────────────────────── +export interface IncomeLedgerRow extends Record { + incomeId: number; + contractId: number; + contractNo: string; + carrierId: number; + carrierName: string; + settleMonth: string; + premiumAmount: number; + companyRate: number; + incomeAmount: number; + commissionType: string; + status: string; +} + +export interface MarginSummaryRow extends Record { + marginId: number; + settleMonth: string; + agentId?: number; + agentName?: string; + incomeTotal: number; + payoutTotal: number; + marginAmount: number; + marginRate?: number; +} + +// ── P7: ReferralCommission ──────────────────────── +export interface ReferralRuleRow extends Record { + ruleId: number; + referralType: string; + calcType: string; + commissionRate?: number; + fixedAmount?: number; + effectiveFrom: string; + effectiveTo?: string; +} + +export interface ReferralLedgerRow extends Record { + ledgerId: number; + contractId: number; + contractNo: string; + referrerAgentId: number; + referrerAgentName: string; + settleMonth: string; + referralType: string; + baseAmount: number; + commissionRate?: number; + commissionAmount: number; + status: string; +} + +// ── P7: SettlingSupport ─────────────────────────── +export interface SettlingSupportRuleRow extends Record { + ruleId: number; + agentGrade: string; + supportAmount: number; + durationMonths: number; + clawbackMonths: number; + effectiveFrom: string; + effectiveTo?: string; +} + +export interface SettlingSupportLedgerRow extends Record { + ledgerId: number; + agentId: number; + agentName: string; + settleMonth: string; + supportAmount: number; + clawbackAmount: number; + netAmount: number; + status: string; +} + +// ── P7: RecognizedPerformance ───────────────────── +export interface ConversionRateRow extends Record { + rateId: number; + productId: number; + productName: string; + conversionRate: number; + effectiveFrom: string; + effectiveTo?: string; +} + +export interface RecognizedLedgerRow extends Record { + ledgerId: number; + agentId: number; + agentName: string; + contractId: number; + contractNo: string; + settleMonth: string; + premiumAmount: number; + conversionRate: number; + convertedPremium: number; + recognitionRate: number; + recognizedAmount: number; + status: string; +} + +// ── P7: Award ───────────────────────────────────── +export interface AwardRuleRow extends Record { + ruleId: number; + awardType: string; + calcType: string; + thresholdAmount?: number; + awardRate?: number; + awardFixed?: number; + effectiveFrom: string; + effectiveTo?: string; +} + +export interface AwardLedgerRow extends Record { + ledgerId: number; + agentId: number; + agentName: string; + settleMonth: string; + awardType: string; + baseAmount: number; + awardAmount: number; + status: string; +} + +// ── P7: LifeSupport ─────────────────────────────── +export interface LifeSupportRuleRow extends Record { + ruleId: number; + agentGrade: string; + supportRate: number; + effectiveFrom: string; + effectiveTo?: string; +} + +export interface LifeSupportLedgerRow extends Record { + ledgerId: number; + agentId: number; + agentName: string; + settleMonth: string; + baseAmount: number; + supportRate: number; + supportAmount: number; + clawbackAmount: number; + netAmount: number; + status: string; +} + +// ── P7: BranchAllowance ─────────────────────────── +export interface BranchAllowanceRuleRow extends Record { + ruleId: number; + positionCode: string; + allowanceRate: number; + capAmount?: number; + effectiveFrom: string; + effectiveTo?: string; +} + +export interface BranchAllowanceLedgerRow extends Record { + ledgerId: number; + managerAgentId: number; + managerAgentName: string; + orgId: number; + orgName: string; + settleMonth: string; + baseAmount: number; + allowanceRate: number; + allowanceAmount: number; + clawbackAmount: number; + status: string; +} + // ── API ─────────────────────────────────────────── export const commissionApi = { // CommissionMaster @@ -227,4 +394,114 @@ export const commissionApi = { unwrap>( api.get('/api/override-ledgers', { params: p }), ), + + // ── P7: IncomeCommission ────────────────────────── + incomeLedgers: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/income-commissions/ledgers', { params: p }), + ), + incomeMargins: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/income-commissions/margins', { params: p }), + ), + incomeMarginRecalc: (settleMonth: string) => + unwrap( + api.post('/api/income-commissions/margin/recalc', null, { params: { settleMonth } }), + ), + + // ── P7: ReferralCommission ──────────────────────── + referralRules: (p?: Partial) => + unwrap>( + api.get('/api/referral-commissions/rules', { params: p }), + ), + referralRuleCreate: (body: Partial) => + unwrap(api.post('/api/referral-commissions/rules', body)), + referralRuleUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/referral-commissions/rules/${id}`, body)), + referralRuleDelete: (id: number) => + unwrap(api.delete(`/api/referral-commissions/rules/${id}`)), + referralLedgers: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/referral-commissions/ledgers', { params: p }), + ), + + // ── P7: SettlingSupport ─────────────────────────── + settlingRules: (p?: Partial) => + unwrap>( + api.get('/api/settling-supports/rules', { params: p }), + ), + settlingRuleCreate: (body: Partial) => + unwrap(api.post('/api/settling-supports/rules', body)), + settlingRuleUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/settling-supports/rules/${id}`, body)), + settlingRuleDelete: (id: number) => + unwrap(api.delete(`/api/settling-supports/rules/${id}`)), + settlingLedgers: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/settling-supports/ledgers', { params: p }), + ), + + // ── P7: RecognizedPerformance ───────────────────── + conversionRates: (p?: Partial) => + unwrap>( + api.get('/api/recognized-performances/rates', { params: p }), + ), + conversionRateCreate: (body: Partial) => + unwrap(api.post('/api/recognized-performances/rates', body)), + conversionRateUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/recognized-performances/rates/${id}`, body)), + conversionRateDelete: (id: number) => + unwrap(api.delete(`/api/recognized-performances/rates/${id}`)), + recognizedLedgers: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/recognized-performances/ledgers', { params: p }), + ), + + // ── P7: Award ───────────────────────────────────── + awardRules: (p?: Partial) => + unwrap>( + api.get('/api/awards/rules', { params: p }), + ), + awardRuleCreate: (body: Partial) => + unwrap(api.post('/api/awards/rules', body)), + awardRuleUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/awards/rules/${id}`, body)), + awardRuleDelete: (id: number) => + unwrap(api.delete(`/api/awards/rules/${id}`)), + awardLedgers: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/awards/ledgers', { params: p }), + ), + + // ── P7: LifeSupport ─────────────────────────────── + lifeSupportRules: (p?: Partial) => + unwrap>( + api.get('/api/life-supports/rules', { params: p }), + ), + lifeSupportRuleCreate: (body: Partial) => + unwrap(api.post('/api/life-supports/rules', body)), + lifeSupportRuleUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/life-supports/rules/${id}`, body)), + lifeSupportRuleDelete: (id: number) => + unwrap(api.delete(`/api/life-supports/rules/${id}`)), + lifeSupportLedgers: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/life-supports/ledgers', { params: p }), + ), + + // ── P7: BranchAllowance ─────────────────────────── + branchAllowanceRules: (p?: Partial) => + unwrap>( + api.get('/api/branch-allowances/rules', { params: p }), + ), + branchAllowanceRuleCreate: (body: Partial) => + unwrap(api.post('/api/branch-allowances/rules', body)), + branchAllowanceRuleUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/branch-allowances/rules/${id}`, body)), + branchAllowanceRuleDelete: (id: number) => + unwrap(api.delete(`/api/branch-allowances/rules/${id}`)), + branchAllowanceLedgers: (p: CommissionSearchParam) => + unwrap>( + api.get('/api/branch-allowances/ledgers', { params: p }), + ), }; diff --git a/ga-frontend/src/api/operation.ts b/ga-frontend/src/api/operation.ts new file mode 100644 index 0000000..ad0a54f --- /dev/null +++ b/ga-frontend/src/api/operation.ts @@ -0,0 +1,229 @@ +import api, { PageResponse, unwrap } from './request'; + +// ── 단계마감 (StepClose) ───────────────────────────── +export interface StepCloseRow extends Record { + stepCloseId: number; + settleMonth: string; + stepCode: string; + stepSeq: number; + status: string; + closedAt?: string; + closedBy?: number; + createdAt?: string; +} + +// ── 본사정산 (HqSettle) ────────────────────────────── +export interface HqSettleRow extends Record { + itemId: number; + settleMonth: string; + itemType: string; + itemName: string; + amount: number; + memo?: string; + status: string; + createdAt?: string; +} + +export interface HqSettleSummary { + incomeTotal: number; + expenseTotal: number; + profitLoss: number; +} + +// ── 등급평가 (GradeEvaluation) ─────────────────────── +export interface GradeEvalRuleRow extends Record { + ruleId: number; + gradeId: number; + gradeName?: string; + minScore: number; + effectiveFrom: string; + effectiveTo?: string; +} + +export interface GradeEvalRow extends Record { + evalId: number; + agentId: number; + agentName?: string; + evalPeriod: string; + performanceScore: number; + prevGradeId?: number; + prevGradeName?: string; + evaluatedGradeId?: number; + evaluatedGradeName?: string; + status: string; +} + +// ── 공동계약 (JointContract) ───────────────────────── +export interface JointContractRow extends Record { + jointId: number; + contractId: number; + contractNo?: string; + agentId: number; + agentName?: string; + role: string; + shareRate: number; + createdAt?: string; +} + +// ── 보증보험 (GuaranteeInsurance) ──────────────────── +export interface GuaranteeInsuranceRow extends Record { + guaranteeId: number; + agentId: number; + agentName?: string; + policyNo: string; + carrierId?: number; + carrierName?: string; + guaranteeAmount: number; + startDate: string; + endDate: string; + status: string; +} + +// ── 적립금 (ReserveFund) ───────────────────────────── +export interface ReserveFundRow extends Record { + reserveId: number; + agentId: number; + agentName?: string; + settleMonth: string; + reserveIn: number; + reserveOut: number; + balance: number; + createdAt?: string; +} + +// ── 수수료 시뮬레이터 (CommissionSimulation) ────────── +export interface SimulationRow extends Record { + simId: number; + simName: string; + premiumAmount: number; + companyRate: number; + payoutRate: number; + commissionYear: number; + companyPayment: number; + agentCommission: number; + withholdingTax: number; + netAmount: number; + expectedChargeback?: number; + createdAt?: string; +} + +export interface SimulationRequest { + simName: string; + premiumAmount: number; + companyRate: number; + payoutRate: number; + commissionYear: number; +} + +// ── API ─────────────────────────────────────────────── +export const operationApi = { + // StepClose + stepCloseList: (params?: { settleMonth?: string }) => + unwrap>( + api.get('/api/step-closes', { params }), + ), + stepCloseByMonth: (settleMonth: string) => + unwrap( + api.get('/api/step-closes/by-month', { params: { settleMonth } }), + ), + stepCloseGet: (id: number) => + unwrap(api.get(`/api/step-closes/${id}`)), + stepCloseCreate: (body: Partial) => + unwrap(api.post('/api/step-closes', body)), + stepCloseClose: (id: number) => + unwrap(api.post(`/api/step-closes/${id}/close`)), + stepCloseReopen: (id: number) => + unwrap(api.post(`/api/step-closes/${id}/reopen`)), + + // HqSettle + hqSettleList: (params?: { settleMonth?: string }) => + unwrap>( + api.get('/api/hq-settles', { params }), + ), + hqSettleSummary: (settleMonth: string) => + unwrap( + api.get('/api/hq-settles/summary', { params: { settleMonth } }), + ), + hqSettleGet: (id: number) => + unwrap(api.get(`/api/hq-settles/${id}`)), + hqSettleCreate: (body: Partial) => + unwrap(api.post('/api/hq-settles', body)), + hqSettleUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/hq-settles/${id}`, body)), + hqSettleDelete: (id: number) => + unwrap(api.delete(`/api/hq-settles/${id}`)), + + // GradeEval Rules + gradeEvalRules: () => + unwrap(api.get('/api/grade-evaluations/rules')), + gradeEvalRuleGet: (id: number) => + unwrap(api.get(`/api/grade-evaluations/rules/${id}`)), + gradeEvalRuleCreate: (body: Partial) => + unwrap(api.post('/api/grade-evaluations/rules', body)), + gradeEvalRuleUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/grade-evaluations/rules/${id}`, body)), + gradeEvalRuleDelete: (id: number) => + unwrap(api.delete(`/api/grade-evaluations/rules/${id}`)), + + // GradeEval Evaluations + gradeEvalList: (params?: { agentName?: string; evalPeriod?: string }) => + unwrap>( + api.get('/api/grade-evaluations', { params }), + ), + gradeEvalGet: (evalId: number) => + unwrap(api.get(`/api/grade-evaluations/${evalId}`)), + gradeEvalCreate: (body: Partial) => + unwrap(api.post('/api/grade-evaluations', body)), + gradeEvalApply: (evalId: number) => + unwrap(api.post(`/api/grade-evaluations/${evalId}/apply`)), + gradeEvalDelete: (evalId: number) => + unwrap(api.delete(`/api/grade-evaluations/${evalId}`)), + + // JointContract + jointContractList: (params?: { contractNo?: string; agentName?: string }) => + unwrap>( + api.get('/api/joint-contracts', { params }), + ), + jointContractGet: (id: number) => + unwrap(api.get(`/api/joint-contracts/${id}`)), + jointContractCreate: (body: Partial) => + unwrap(api.post('/api/joint-contracts', body)), + jointContractUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/joint-contracts/${id}`, body)), + jointContractDelete: (id: number) => + unwrap(api.delete(`/api/joint-contracts/${id}`)), + + // GuaranteeInsurance + guaranteeInsuranceList: (params?: { agentName?: string; status?: string }) => + unwrap>( + api.get('/api/guarantee-insurances', { params }), + ), + guaranteeInsuranceGet: (id: number) => + unwrap(api.get(`/api/guarantee-insurances/${id}`)), + guaranteeInsuranceCreate: (body: Partial) => + unwrap(api.post('/api/guarantee-insurances', body)), + guaranteeInsuranceUpdate: (id: number, body: Partial) => + unwrap(api.put(`/api/guarantee-insurances/${id}`, body)), + guaranteeInsuranceDelete: (id: number) => + unwrap(api.delete(`/api/guarantee-insurances/${id}`)), + + // ReserveFund + reserveFundList: (params?: { settleMonth?: string; agentName?: string }) => + unwrap>( + api.get('/api/reserve-funds', { params }), + ), + reserveFundGet: (id: number) => + unwrap(api.get(`/api/reserve-funds/${id}`)), + reserveFundUpsert: (body: Partial) => + unwrap(api.post('/api/reserve-funds', body)), + + // CommissionSimulation + simulationList: (params?: Record) => + unwrap>( + api.get('/api/commission-simulations', { params }), + ), + simulationGet: (id: number) => + unwrap(api.get(`/api/commission-simulations/${id}`)), + simulate: (body: SimulationRequest) => + unwrap(api.post('/api/commission-simulations/simulate', body)), +}; diff --git a/ga-frontend/src/pages/commission/Award.tsx b/ga-frontend/src/pages/commission/Award.tsx new file mode 100644 index 0000000..177e569 --- /dev/null +++ b/ga-frontend/src/pages/commission/Award.tsx @@ -0,0 +1,209 @@ +import { useState } from 'react'; +import { Alert, Form, Input, InputNumber, Modal, Select, Space, Tabs, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { + commissionApi, + AwardRuleRow, + AwardLedgerRow, + CommissionSearchParam, +} from '@/api/commission'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; + +const AWARD_TYPE_OPTIONS = [ + { value: 'NEW_RECRUIT', label: '신인도입' }, + { value: 'CORPORATE', label: '법인' }, + { value: 'RETENTION', label: '유지율' }, + { value: 'PERFORMANCE', label: '실적' }, +]; + +const RULE_COLS: ColDef[] = [ + { field: 'awardType', headerName: '시상유형', width: 130 }, + { field: 'calcType', headerName: '계산방식', width: 110 }, + { field: 'thresholdAmount', headerName: '기준실적', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'awardRate', headerName: '시상율', width: 110, type: 'numericColumn' }, + { field: 'awardFixed', headerName: '시상정액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'effectiveFrom', headerName: '적용시작', width: 120 }, + { field: 'effectiveTo', headerName: '적용종료', width: 120 }, +]; + +const LEDGER_COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 130 }, + { field: 'awardType', headerName: '시상유형', width: 120 }, + { field: 'baseAmount', headerName: '기준실적', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'awardAmount', headerName: '시상금', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'status', headerName: '상태', width: 100 }, +]; + +export default function Award() { + const qc = useQueryClient(); + const [ledgerParams, setLedgerParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), pageSize: 200, + }); + const [ruleModalOpen, setRuleModalOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); + const [form] = Form.useForm(); + + const { data: rulesData, isLoading: rulesLoading, isError: rulesError } = useQuery({ + queryKey: ['award', 'rules'], + queryFn: () => commissionApi.awardRules(), + retry: false, + }); + + const { data: ledgersData, isLoading: ledgersLoading } = useQuery({ + queryKey: ['award', 'ledgers', ledgerParams], + queryFn: () => commissionApi.awardLedgers(ledgerParams), + retry: false, + }); + + const rules = rulesData?.list ?? []; + const ledgers = ledgersData?.list ?? []; + + const openCreate = () => { setEditingRule(null); form.resetFields(); setRuleModalOpen(true); }; + const openEdit = (row: AwardRuleRow) => { setEditingRule(row); form.setFieldsValue(row); setRuleModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + try { + if (editingRule) { + await commissionApi.awardRuleUpdate(editingRule.ruleId, values); + message.success('수정 완료'); + } else { + await commissionApi.awardRuleCreate(values); + message.success('등록 완료'); + } + qc.invalidateQueries({ queryKey: ['award', 'rules'] }); + setRuleModalOpen(false); + } catch { /* request.ts 처리 */ } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '룰 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await commissionApi.awardRuleDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['award', 'rules'] }); + }, + }); + + const pinnedLedger = ledgers.length > 0 ? { + agentName: '합계', + baseAmount: ledgers.reduce((s, r) => s + (r.baseAmount ?? 0), 0), + awardAmount: ledgers.reduce((s, r) => s + (r.awardAmount ?? 0), 0), + } as Partial : undefined; + + const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + + return ( + + {rulesError && ( + + )} + + +
+ + + 룰 등록 + +
+ + rows={rules} + columns={[ + ...RULE_COLS, + { + headerName: '액션', width: 160, pinned: 'right', + cellRenderer: (p: { data: AwardRuleRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.ruleId)}>삭제 + + ), + }, + ]} + loading={rulesLoading} + height={520} + rowKey="ruleId" + /> + + ), + }, + { + key: 'ledgers', + label: '원장', + children: ( + <> + setLedgerParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setLedgerParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + + rows={ledgers} + columns={LEDGER_COLS} + loading={ledgersLoading} + height={520} + rowKey="ledgerId" + pinnedBottomRow={pinnedLedger} + /> + + + ), + }, + ]} + /> + + setRuleModalOpen(false)} + okText="저장" + cancelText="취소" + > +
+ + + + + + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/commission/BranchAllowance.tsx b/ga-frontend/src/pages/commission/BranchAllowance.tsx new file mode 100644 index 0000000..8c30cd3 --- /dev/null +++ b/ga-frontend/src/pages/commission/BranchAllowance.tsx @@ -0,0 +1,198 @@ +import { useState } from 'react'; +import { Alert, Form, Input, InputNumber, Modal, Space, Tabs, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { + commissionApi, + BranchAllowanceRuleRow, + BranchAllowanceLedgerRow, + CommissionSearchParam, +} from '@/api/commission'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; +const fmtRate = (v: unknown) => v != null ? `${((v as number) * 100).toFixed(2)}%` : '-'; + +const RULE_COLS: ColDef[] = [ + { field: 'positionCode', headerName: '직책코드', width: 130 }, + { field: 'allowanceRate', headerName: '수당율', width: 110, type: 'numericColumn', valueFormatter: (p) => fmtRate(p.value) }, + { field: 'capAmount', headerName: '상한', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'effectiveFrom', headerName: '적용시작', width: 120 }, + { field: 'effectiveTo', headerName: '적용종료', width: 120 }, +]; + +const LEDGER_COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'managerAgentName',headerName: '지점장', width: 130 }, + { field: 'orgName', headerName: '조직명', width: 130 }, + { field: 'baseAmount', headerName: '조직실적', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'allowanceRate', headerName: '수당율', width: 110, type: 'numericColumn', valueFormatter: (p) => fmtRate(p.value) }, + { field: 'allowanceAmount', headerName: '수당금액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'clawbackAmount', headerName: '환수액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'status', headerName: '상태', width: 100 }, +]; + +export default function BranchAllowance() { + const qc = useQueryClient(); + const [ledgerParams, setLedgerParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), pageSize: 200, + }); + const [ruleModalOpen, setRuleModalOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); + const [form] = Form.useForm(); + + const { data: rulesData, isLoading: rulesLoading, isError: rulesError } = useQuery({ + queryKey: ['branchAllowance', 'rules'], + queryFn: () => commissionApi.branchAllowanceRules(), + retry: false, + }); + + const { data: ledgersData, isLoading: ledgersLoading } = useQuery({ + queryKey: ['branchAllowance', 'ledgers', ledgerParams], + queryFn: () => commissionApi.branchAllowanceLedgers(ledgerParams), + retry: false, + }); + + const rules = rulesData?.list ?? []; + const ledgers = ledgersData?.list ?? []; + + const openCreate = () => { setEditingRule(null); form.resetFields(); setRuleModalOpen(true); }; + const openEdit = (row: BranchAllowanceRuleRow) => { setEditingRule(row); form.setFieldsValue(row); setRuleModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + try { + if (editingRule) { + await commissionApi.branchAllowanceRuleUpdate(editingRule.ruleId, values); + message.success('수정 완료'); + } else { + await commissionApi.branchAllowanceRuleCreate(values); + message.success('등록 완료'); + } + qc.invalidateQueries({ queryKey: ['branchAllowance', 'rules'] }); + setRuleModalOpen(false); + } catch { /* request.ts 처리 */ } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '룰 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await commissionApi.branchAllowanceRuleDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['branchAllowance', 'rules'] }); + }, + }); + + const pinnedLedger = ledgers.length > 0 ? { + managerAgentName: '합계', + baseAmount: ledgers.reduce((s, r) => s + (r.baseAmount ?? 0), 0), + allowanceAmount: ledgers.reduce((s, r) => s + (r.allowanceAmount ?? 0), 0), + clawbackAmount: ledgers.reduce((s, r) => s + (r.clawbackAmount ?? 0), 0), + } as Partial : undefined; + + const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + + return ( + + {rulesError && ( + + )} + + +
+ + + 룰 등록 + +
+ + rows={rules} + columns={[ + ...RULE_COLS, + { + headerName: '액션', width: 160, pinned: 'right', + cellRenderer: (p: { data: BranchAllowanceRuleRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.ruleId)}>삭제 + + ), + }, + ]} + loading={rulesLoading} + height={520} + rowKey="ruleId" + /> + + ), + }, + { + key: 'ledgers', + label: '원장', + children: ( + <> + setLedgerParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setLedgerParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + + rows={ledgers} + columns={LEDGER_COLS} + loading={ledgersLoading} + height={520} + rowKey="ledgerId" + pinnedBottomRow={pinnedLedger} + /> + + + ), + }, + ]} + /> + + setRuleModalOpen(false)} + okText="저장" + cancelText="취소" + > +
+ + + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/commission/CommissionSimulator.tsx b/ga-frontend/src/pages/commission/CommissionSimulator.tsx new file mode 100644 index 0000000..9abf10c --- /dev/null +++ b/ga-frontend/src/pages/commission/CommissionSimulator.tsx @@ -0,0 +1,147 @@ +import { useState } from 'react'; +import { Alert, Card, Col, Form, Input, InputNumber, Row, Statistic, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import PageContainer from '@/components/common/PageContainer'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { operationApi, SimulationRow, SimulationRequest } from '@/api/operation'; +import { GRAY, RADIUS, SHADOW, COLOR_PRIMARY, COLOR_ERROR, COLOR_SUCCESS } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; +const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + +const HIST_COLS: ColDef[] = [ + { field: 'simName', headerName: '시나리오명', width: 150 }, + { field: 'premiumAmount', headerName: '보험료', width: 130, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'companyRate', headerName: '보험사요율(%)', width: 130, type: 'numericColumn', + valueFormatter: (p) => `${((p.value as number) * 100).toFixed(2)}%` }, + { field: 'payoutRate', headerName: '지급률(%)', width: 110, type: 'numericColumn', + valueFormatter: (p) => `${((p.value as number) * 100).toFixed(2)}%` }, + { field: 'commissionYear', headerName: '회차', width: 70, type: 'numericColumn' }, + { field: 'companyPayment', headerName: '보험사지급액', width: 140, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'agentCommission', headerName: '설계사수수료', width: 140, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'withholdingTax', headerName: '원천세(3.3%)', width: 130, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'netAmount', headerName: '실지급', width: 120, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'expectedChargeback', headerName: '예상환수', width: 120, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'createdAt', headerName: '계산일시', flex: 1 }, +]; + +export default function CommissionSimulator() { + const qc = useQueryClient(); + const [form] = Form.useForm(); + const [result, setResult] = useState(null); + + const { data: listData, isLoading, isError } = useQuery({ + queryKey: ['commission-simulation', 'list'], + queryFn: () => operationApi.simulationList(), + retry: false, + }); + + const rows = listData?.list ?? []; + + const handleSimulate = async () => { + const values = await form.validateFields(); + // UI: 율은 % 단위 → 백엔드는 0~1 + const body: SimulationRequest = { + simName: values.simName as string, + premiumAmount: values.premiumAmount as number, + companyRate: (values.companyRate as number) / 100, + payoutRate: (values.payoutRate as number) / 100, + commissionYear: values.commissionYear as number, + }; + try { + const res = await operationApi.simulate(body); + setResult(res); + message.success('계산 완료'); + qc.invalidateQueries({ queryKey: ['commission-simulation', 'list'] }); + } catch { /* request.ts 처리 */ } + }; + + return ( + + {isError && ( + + )} + + {/* 입력 폼 */} + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 계산 + +
+
+ + {/* 결과 카드 */} + {result && ( + + + + (v as number).toLocaleString()} suffix="원" valueStyle={{ color: COLOR_PRIMARY }} /> + + + + + (v as number).toLocaleString()} suffix="원" valueStyle={{ color: COLOR_PRIMARY }} /> + + + + + (v as number).toLocaleString()} suffix="원" valueStyle={{ color: COLOR_ERROR }} /> + + + + + (v as number).toLocaleString()} suffix="원" valueStyle={{ color: COLOR_SUCCESS }} /> + + + + + (v as number).toLocaleString()} suffix="원" valueStyle={{ color: COLOR_ERROR }} /> + + + + )} + + {/* 저장된 시뮬 목록 */} + + + rows={rows} + columns={HIST_COLS} + loading={isLoading} + height={400} + rowKey="simId" + /> + +
+ ); +} diff --git a/ga-frontend/src/pages/commission/IncomeCommission.tsx b/ga-frontend/src/pages/commission/IncomeCommission.tsx new file mode 100644 index 0000000..cd6be43 --- /dev/null +++ b/ga-frontend/src/pages/commission/IncomeCommission.tsx @@ -0,0 +1,168 @@ +import { useState } from 'react'; +import { Alert, Button, Space, Tabs, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { + commissionApi, + IncomeLedgerRow, + MarginSummaryRow, + CommissionSearchParam, +} from '@/api/commission'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; +const fmtRate = (v: unknown) => v != null ? `${((v as number) * 100).toFixed(2)}%` : '-'; + +const LEDGER_COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'contractNo', headerName: '계약번호', width: 140 }, + { field: 'carrierName', headerName: '보험사', width: 130 }, + { field: 'commissionType', headerName: '수수료유형', width: 120 }, + { field: 'premiumAmount', headerName: '보험료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'companyRate', headerName: '보험사요율', width: 110, type: 'numericColumn', valueFormatter: (p) => fmtRate(p.value) }, + { field: 'incomeAmount', headerName: '수입수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'status', headerName: '상태', width: 100 }, +]; + +const MARGIN_COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 130 }, + { field: 'incomeTotal', headerName: '수입합계', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'payoutTotal', headerName: '지급합계', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'marginAmount', headerName: '수지차', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'marginRate', headerName: '수지차율', width: 110, type: 'numericColumn', valueFormatter: (p) => fmtRate(p.value) }, +]; + +export default function IncomeCommission() { + const qc = useQueryClient(); + const [ledgerParams, setLedgerParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), pageSize: 200, + }); + const [marginParams, setMarginParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), pageSize: 200, + }); + const [recalcLoading, setRecalcLoading] = useState(false); + + const { data: ledgersData, isLoading: ledgersLoading, isError: ledgersError } = useQuery({ + queryKey: ['income', 'ledgers', ledgerParams], + queryFn: () => commissionApi.incomeLedgers(ledgerParams), + retry: false, + }); + + const { data: marginData, isLoading: marginLoading } = useQuery({ + queryKey: ['income', 'margin', marginParams], + queryFn: () => commissionApi.incomeMargins(marginParams), + retry: false, + }); + + const ledgers = ledgersData?.list ?? []; + const margins = marginData?.list ?? []; + + const pinnedLedger = ledgers.length > 0 ? { + contractNo: '합계', + premiumAmount: ledgers.reduce((s, r) => s + (r.premiumAmount ?? 0), 0), + incomeAmount: ledgers.reduce((s, r) => s + (r.incomeAmount ?? 0), 0), + } as Partial : undefined; + + const pinnedMargin = margins.length > 0 ? { + agentName: '합계', + incomeTotal: margins.reduce((s, r) => s + (r.incomeTotal ?? 0), 0), + payoutTotal: margins.reduce((s, r) => s + (r.payoutTotal ?? 0), 0), + marginAmount: margins.reduce((s, r) => s + (r.marginAmount ?? 0), 0), + } as Partial : undefined; + + const handleRecalc = async () => { + setRecalcLoading(true); + try { + await commissionApi.incomeMarginRecalc(marginParams.settleMonth ?? dayjs().format('YYYYMM')); + message.success('수지차 재계산 완료'); + qc.invalidateQueries({ queryKey: ['income', 'margin'] }); + } catch { /* request.ts 처리 */ } + finally { setRecalcLoading(false); } + }; + + const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + + return ( + + {ledgersError && ( + + )} + + + setLedgerParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setLedgerParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + + rows={ledgers} + columns={LEDGER_COLS} + loading={ledgersLoading} + height={520} + rowKey="incomeId" + pinnedBottomRow={pinnedLedger} + /> + + + ), + }, + { + key: 'margin', + label: '수지차(margin)', + children: ( + <> + setMarginParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setMarginParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + +
+ + 수지차 재계산 + +
+ + rows={margins} + columns={MARGIN_COLS} + loading={marginLoading} + height={520} + rowKey="marginId" + pinnedBottomRow={pinnedMargin} + /> +
+ + ), + }, + ]} + /> +
+ ); +} diff --git a/ga-frontend/src/pages/commission/LifeSupport.tsx b/ga-frontend/src/pages/commission/LifeSupport.tsx new file mode 100644 index 0000000..464e91e --- /dev/null +++ b/ga-frontend/src/pages/commission/LifeSupport.tsx @@ -0,0 +1,195 @@ +import { useState } from 'react'; +import { Alert, Form, Input, InputNumber, Modal, Space, Tabs, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { + commissionApi, + LifeSupportRuleRow, + LifeSupportLedgerRow, + CommissionSearchParam, +} from '@/api/commission'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; +const fmtRate = (v: unknown) => v != null ? `${((v as number) * 100).toFixed(2)}%` : '-'; + +const RULE_COLS: ColDef[] = [ + { field: 'agentGrade', headerName: '설계사등급', width: 130 }, + { field: 'supportRate', headerName: '지급률', width: 110, type: 'numericColumn', valueFormatter: (p) => fmtRate(p.value) }, + { field: 'effectiveFrom',headerName: '적용시작', width: 120 }, + { field: 'effectiveTo', headerName: '적용종료', width: 120 }, +]; + +const LEDGER_COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 130 }, + { field: 'baseAmount', headerName: '생보실적', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'supportRate', headerName: '지급률', width: 110, type: 'numericColumn', valueFormatter: (p) => fmtRate(p.value) }, + { field: 'supportAmount', headerName: '지급액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'clawbackAmount', headerName: '환수액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'netAmount', headerName: '실지급액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'status', headerName: '상태', width: 100 }, +]; + +export default function LifeSupport() { + const qc = useQueryClient(); + const [ledgerParams, setLedgerParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), pageSize: 200, + }); + const [ruleModalOpen, setRuleModalOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); + const [form] = Form.useForm(); + + const { data: rulesData, isLoading: rulesLoading, isError: rulesError } = useQuery({ + queryKey: ['lifeSupport', 'rules'], + queryFn: () => commissionApi.lifeSupportRules(), + retry: false, + }); + + const { data: ledgersData, isLoading: ledgersLoading } = useQuery({ + queryKey: ['lifeSupport', 'ledgers', ledgerParams], + queryFn: () => commissionApi.lifeSupportLedgers(ledgerParams), + retry: false, + }); + + const rules = rulesData?.list ?? []; + const ledgers = ledgersData?.list ?? []; + + const openCreate = () => { setEditingRule(null); form.resetFields(); setRuleModalOpen(true); }; + const openEdit = (row: LifeSupportRuleRow) => { setEditingRule(row); form.setFieldsValue(row); setRuleModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + try { + if (editingRule) { + await commissionApi.lifeSupportRuleUpdate(editingRule.ruleId, values); + message.success('수정 완료'); + } else { + await commissionApi.lifeSupportRuleCreate(values); + message.success('등록 완료'); + } + qc.invalidateQueries({ queryKey: ['lifeSupport', 'rules'] }); + setRuleModalOpen(false); + } catch { /* request.ts 처리 */ } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '룰 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await commissionApi.lifeSupportRuleDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['lifeSupport', 'rules'] }); + }, + }); + + const pinnedLedger = ledgers.length > 0 ? { + agentName: '합계', + baseAmount: ledgers.reduce((s, r) => s + (r.baseAmount ?? 0), 0), + supportAmount: ledgers.reduce((s, r) => s + (r.supportAmount ?? 0), 0), + clawbackAmount: ledgers.reduce((s, r) => s + (r.clawbackAmount ?? 0), 0), + netAmount: ledgers.reduce((s, r) => s + (r.netAmount ?? 0), 0), + } as Partial : undefined; + + const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + + return ( + + {rulesError && ( + + )} + + +
+ + + 룰 등록 + +
+ + rows={rules} + columns={[ + ...RULE_COLS, + { + headerName: '액션', width: 160, pinned: 'right', + cellRenderer: (p: { data: LifeSupportRuleRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.ruleId)}>삭제 + + ), + }, + ]} + loading={rulesLoading} + height={520} + rowKey="ruleId" + /> + + ), + }, + { + key: 'ledgers', + label: '원장', + children: ( + <> + setLedgerParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setLedgerParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + + rows={ledgers} + columns={LEDGER_COLS} + loading={ledgersLoading} + height={520} + rowKey="ledgerId" + pinnedBottomRow={pinnedLedger} + /> + + + ), + }, + ]} + /> + + setRuleModalOpen(false)} + okText="저장" + cancelText="취소" + > +
+ + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/commission/RecognizedPerformance.tsx b/ga-frontend/src/pages/commission/RecognizedPerformance.tsx new file mode 100644 index 0000000..6836757 --- /dev/null +++ b/ga-frontend/src/pages/commission/RecognizedPerformance.tsx @@ -0,0 +1,195 @@ +import { useState } from 'react'; +import { Alert, Form, Input, InputNumber, Modal, Space, Tabs, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { + commissionApi, + ConversionRateRow, + RecognizedLedgerRow, + CommissionSearchParam, +} from '@/api/commission'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; +const fmtRate = (v: unknown) => v != null ? `${((v as number) * 100).toFixed(2)}%` : '-'; + +const RULE_COLS: ColDef[] = [ + { field: 'productName', headerName: '상품명', width: 180 }, + { field: 'conversionRate', headerName: '환산율', width: 110, type: 'numericColumn', valueFormatter: (p) => fmtRate(p.value) }, + { field: 'effectiveFrom', headerName: '적용시작', width: 120 }, + { field: 'effectiveTo', headerName: '적용종료', width: 120 }, +]; + +const LEDGER_COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 130 }, + { field: 'contractNo', headerName: '계약번호', width: 140 }, + { field: 'premiumAmount', headerName: '보험료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'conversionRate', headerName: '환산율', width: 110, type: 'numericColumn', valueFormatter: (p) => fmtRate(p.value) }, + { field: 'convertedPremium', headerName: '환산보험료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'recognitionRate', headerName: '인정율', width: 110, type: 'numericColumn', valueFormatter: (p) => fmtRate(p.value) }, + { field: 'recognizedAmount', headerName: '인정실적', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'status', headerName: '상태', width: 100 }, +]; + +export default function RecognizedPerformance() { + const qc = useQueryClient(); + const [ledgerParams, setLedgerParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), pageSize: 200, + }); + const [ruleModalOpen, setRuleModalOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); + const [form] = Form.useForm(); + + const { data: rulesData, isLoading: rulesLoading, isError: rulesError } = useQuery({ + queryKey: ['recognized', 'rules'], + queryFn: () => commissionApi.conversionRates(), + retry: false, + }); + + const { data: ledgersData, isLoading: ledgersLoading } = useQuery({ + queryKey: ['recognized', 'ledgers', ledgerParams], + queryFn: () => commissionApi.recognizedLedgers(ledgerParams), + retry: false, + }); + + const rules = rulesData?.list ?? []; + const ledgers = ledgersData?.list ?? []; + + const openCreate = () => { setEditingRule(null); form.resetFields(); setRuleModalOpen(true); }; + const openEdit = (row: ConversionRateRow) => { setEditingRule(row); form.setFieldsValue(row); setRuleModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + try { + if (editingRule) { + await commissionApi.conversionRateUpdate(editingRule.rateId, values); + message.success('수정 완료'); + } else { + await commissionApi.conversionRateCreate(values); + message.success('등록 완료'); + } + qc.invalidateQueries({ queryKey: ['recognized', 'rules'] }); + setRuleModalOpen(false); + } catch { /* request.ts 처리 */ } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '환산율 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await commissionApi.conversionRateDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['recognized', 'rules'] }); + }, + }); + + const pinnedLedger = ledgers.length > 0 ? { + agentName: '합계', + premiumAmount: ledgers.reduce((s, r) => s + (r.premiumAmount ?? 0), 0), + convertedPremium: ledgers.reduce((s, r) => s + (r.convertedPremium ?? 0), 0), + recognizedAmount: ledgers.reduce((s, r) => s + (r.recognizedAmount ?? 0), 0), + } as Partial : undefined; + + const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + + return ( + + {rulesError && ( + + )} + + +
+ + + 환산율 등록 + +
+ + rows={rules} + columns={[ + ...RULE_COLS, + { + headerName: '액션', width: 160, pinned: 'right', + cellRenderer: (p: { data: ConversionRateRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.rateId)}>삭제 + + ), + }, + ]} + loading={rulesLoading} + height={520} + rowKey="rateId" + /> + + ), + }, + { + key: 'ledgers', + label: '인정실적원장', + children: ( + <> + setLedgerParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setLedgerParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + + rows={ledgers} + columns={LEDGER_COLS} + loading={ledgersLoading} + height={520} + rowKey="ledgerId" + pinnedBottomRow={pinnedLedger} + /> + + + ), + }, + ]} + /> + + setRuleModalOpen(false)} + okText="저장" + cancelText="취소" + > +
+ + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/commission/ReferralCommission.tsx b/ga-frontend/src/pages/commission/ReferralCommission.tsx new file mode 100644 index 0000000..b5e6c58 --- /dev/null +++ b/ga-frontend/src/pages/commission/ReferralCommission.tsx @@ -0,0 +1,200 @@ +import { useState } from 'react'; +import { Alert, Form, Input, InputNumber, Modal, Select, Space, Tabs, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { + commissionApi, + ReferralRuleRow, + ReferralLedgerRow, + CommissionSearchParam, +} from '@/api/commission'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; + +const RULE_COLS: ColDef[] = [ + { field: 'referralType', headerName: '유형', width: 120 }, + { field: 'calcType', headerName: '계산방식', width: 110 }, + { field: 'commissionRate', headerName: '수수료율', width: 110, type: 'numericColumn' }, + { field: 'fixedAmount', headerName: '정액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'effectiveFrom', headerName: '적용시작', width: 120 }, + { field: 'effectiveTo', headerName: '적용종료', width: 120 }, +]; + +const LEDGER_COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'referrerAgentName',headerName: '소개설계사', width: 130 }, + { field: 'contractNo', headerName: '계약번호', width: 140 }, + { field: 'referralType', headerName: '유형', width: 100 }, + { field: 'baseAmount', headerName: '기준금액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'commissionRate', headerName: '수수료율', width: 110, type: 'numericColumn' }, + { field: 'commissionAmount', headerName: '수수료', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'status', headerName: '상태', width: 100 }, +]; + +export default function ReferralCommission() { + const qc = useQueryClient(); + const [ledgerParams, setLedgerParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), pageSize: 200, + }); + const [ruleModalOpen, setRuleModalOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); + const [form] = Form.useForm(); + + const { data: rulesData, isLoading: rulesLoading, isError: rulesError } = useQuery({ + queryKey: ['referral', 'rules'], + queryFn: () => commissionApi.referralRules(), + retry: false, + }); + + const { data: ledgersData, isLoading: ledgersLoading } = useQuery({ + queryKey: ['referral', 'ledgers', ledgerParams], + queryFn: () => commissionApi.referralLedgers(ledgerParams), + retry: false, + }); + + const rules = rulesData?.list ?? []; + const ledgers = ledgersData?.list ?? []; + + const openCreate = () => { setEditingRule(null); form.resetFields(); setRuleModalOpen(true); }; + const openEdit = (row: ReferralRuleRow) => { setEditingRule(row); form.setFieldsValue(row); setRuleModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + try { + if (editingRule) { + await commissionApi.referralRuleUpdate(editingRule.ruleId, values); + message.success('수정 완료'); + } else { + await commissionApi.referralRuleCreate(values); + message.success('등록 완료'); + } + qc.invalidateQueries({ queryKey: ['referral', 'rules'] }); + setRuleModalOpen(false); + } catch { /* request.ts 처리 */ } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '룰 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await commissionApi.referralRuleDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['referral', 'rules'] }); + }, + }); + + const pinnedLedger = ledgers.length > 0 ? { + referrerAgentName: '합계', + baseAmount: ledgers.reduce((s, r) => s + (r.baseAmount ?? 0), 0), + commissionAmount: ledgers.reduce((s, r) => s + (r.commissionAmount ?? 0), 0), + } as Partial : undefined; + + const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + + return ( + + {rulesError && ( + + )} + + +
+ + + 룰 등록 + +
+ + rows={rules} + columns={[ + ...RULE_COLS, + { + headerName: '액션', width: 160, pinned: 'right', + cellRenderer: (p: { data: ReferralRuleRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.ruleId)}>삭제 + + ), + }, + ]} + loading={rulesLoading} + height={520} + rowKey="ruleId" + /> + + ), + }, + { + key: 'ledgers', + label: '원장', + children: ( + <> + setLedgerParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setLedgerParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + + rows={ledgers} + columns={LEDGER_COLS} + loading={ledgersLoading} + height={520} + rowKey="ledgerId" + pinnedBottomRow={pinnedLedger} + /> + + + ), + }, + ]} + /> + + setRuleModalOpen(false)} + okText="저장" + cancelText="취소" + > +
+ + + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/commission/ReserveFund.tsx b/ga-frontend/src/pages/commission/ReserveFund.tsx new file mode 100644 index 0000000..b4775df --- /dev/null +++ b/ga-frontend/src/pages/commission/ReserveFund.tsx @@ -0,0 +1,117 @@ +import { useState } from 'react'; +import { Alert, Form, Input, InputNumber, Modal, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { operationApi, ReserveFundRow } from '@/api/operation'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; +const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + +const COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 130 }, + { field: 'reserveIn', headerName: '적립액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'reserveOut', headerName: '인출액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'balance', headerName: '잔액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'createdAt', headerName: '등록일', width: 130 }, +]; + +export default function ReserveFund() { + const qc = useQueryClient(); + const [searchParams, setSearchParams] = useState<{ settleMonth?: string; agentName?: string }>({ + settleMonth: dayjs().format('YYYYMM'), + }); + const [modalOpen, setModalOpen] = useState(false); + const [form] = Form.useForm(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['reserve-fund', 'list', searchParams], + queryFn: () => operationApi.reserveFundList(searchParams), + retry: false, + }); + + const rows = data?.list ?? []; + + const pinnedRow = rows.length > 0 ? { + agentName: '합계', + reserveIn: rows.reduce((s, r) => s + (r.reserveIn ?? 0), 0), + reserveOut: rows.reduce((s, r) => s + (r.reserveOut ?? 0), 0), + balance: rows.reduce((s, r) => s + (r.balance ?? 0), 0), + } as Partial : undefined; + + const handleUpsert = async () => { + const values = await form.validateFields(); + try { + await operationApi.reserveFundUpsert(values); + message.success('등록/수정 완료'); + qc.invalidateQueries({ queryKey: ['reserve-fund', 'list'] }); + setModalOpen(false); + form.resetFields(); + } catch { /* request.ts 처리 */ } + }; + + return ( + + {isError && ( + + )} + + setSearchParams({ settleMonth: v.settleMonth as string, agentName: v.agentName as string })} + onReset={() => setSearchParams({ settleMonth: dayjs().format('YYYYMM') })} + /> + + +
+ { form.resetFields(); setModalOpen(true); }}> + + 적립금 등록 + +
+ + rows={rows} + columns={COLS} + loading={isLoading} + height={520} + rowKey="reserveId" + pinnedBottomRow={pinnedRow} + /> +
+ + setModalOpen(false)} + okText="저장" + cancelText="취소" + > +
+ + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/commission/SettlingSupport.tsx b/ga-frontend/src/pages/commission/SettlingSupport.tsx new file mode 100644 index 0000000..6b01bdf --- /dev/null +++ b/ga-frontend/src/pages/commission/SettlingSupport.tsx @@ -0,0 +1,199 @@ +import { useState } from 'react'; +import { Alert, Form, Input, InputNumber, Modal, Space, Tabs, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { + commissionApi, + SettlingSupportRuleRow, + SettlingSupportLedgerRow, + CommissionSearchParam, +} from '@/api/commission'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; + +const RULE_COLS: ColDef[] = [ + { field: 'agentGrade', headerName: '설계사등급', width: 130 }, + { field: 'supportAmount', headerName: '월지원금', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'durationMonths', headerName: '지원기간(월)', width: 120, type: 'numericColumn' }, + { field: 'clawbackMonths', headerName: '환수기준(월)', width: 120, type: 'numericColumn' }, + { field: 'effectiveFrom', headerName: '적용시작', width: 120 }, + { field: 'effectiveTo', headerName: '적용종료', width: 120 }, +]; + +const LEDGER_COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110, pinned: 'left' }, + { field: 'agentName', headerName: '설계사', width: 130 }, + { field: 'supportAmount', headerName: '지급액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'clawbackAmount', headerName: '환수액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'netAmount', headerName: '실지급액', flex: 1, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'status', headerName: '상태', width: 110 }, +]; + +export default function SettlingSupport() { + const qc = useQueryClient(); + const [ledgerParams, setLedgerParams] = useState({ + settleMonth: dayjs().format('YYYYMM'), pageSize: 200, + }); + const [ruleModalOpen, setRuleModalOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); + const [form] = Form.useForm(); + + const { data: rulesData, isLoading: rulesLoading, isError: rulesError } = useQuery({ + queryKey: ['settling', 'rules'], + queryFn: () => commissionApi.settlingRules(), + retry: false, + }); + + const { data: ledgersData, isLoading: ledgersLoading } = useQuery({ + queryKey: ['settling', 'ledgers', ledgerParams], + queryFn: () => commissionApi.settlingLedgers(ledgerParams), + retry: false, + }); + + const rules = rulesData?.list ?? []; + const ledgers = ledgersData?.list ?? []; + + const openCreate = () => { setEditingRule(null); form.resetFields(); setRuleModalOpen(true); }; + const openEdit = (row: SettlingSupportRuleRow) => { setEditingRule(row); form.setFieldsValue(row); setRuleModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + try { + if (editingRule) { + await commissionApi.settlingRuleUpdate(editingRule.ruleId, values); + message.success('수정 완료'); + } else { + await commissionApi.settlingRuleCreate(values); + message.success('등록 완료'); + } + qc.invalidateQueries({ queryKey: ['settling', 'rules'] }); + setRuleModalOpen(false); + } catch { /* request.ts 처리 */ } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '룰 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await commissionApi.settlingRuleDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['settling', 'rules'] }); + }, + }); + + const pinnedLedger = ledgers.length > 0 ? { + agentName: '합계', + supportAmount: ledgers.reduce((s, r) => s + (r.supportAmount ?? 0), 0), + clawbackAmount: ledgers.reduce((s, r) => s + (r.clawbackAmount ?? 0), 0), + netAmount: ledgers.reduce((s, r) => s + (r.netAmount ?? 0), 0), + } as Partial : undefined; + + const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + + return ( + + {rulesError && ( + + )} + + +
+ + + 룰 등록 + +
+ + rows={rules} + columns={[ + ...RULE_COLS, + { + headerName: '액션', width: 160, pinned: 'right', + cellRenderer: (p: { data: SettlingSupportRuleRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.ruleId)}>삭제 + + ), + }, + ]} + loading={rulesLoading} + height={520} + rowKey="ruleId" + /> + + ), + }, + { + key: 'ledgers', + label: '원장', + children: ( + <> + setLedgerParams({ ...v as CommissionSearchParam, pageSize: 200 })} + onReset={() => setLedgerParams({ settleMonth: dayjs().format('YYYYMM'), pageSize: 200 })} + /> + + + rows={ledgers} + columns={LEDGER_COLS} + loading={ledgersLoading} + height={520} + rowKey="ledgerId" + pinnedBottomRow={pinnedLedger} + /> + + + ), + }, + ]} + /> + + setRuleModalOpen(false)} + okText="저장" + cancelText="취소" + > +
+ + + + + + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/contracts/JointContract.tsx b/ga-frontend/src/pages/contracts/JointContract.tsx new file mode 100644 index 0000000..dc1a4ec --- /dev/null +++ b/ga-frontend/src/pages/contracts/JointContract.tsx @@ -0,0 +1,145 @@ +import { useState } from 'react'; +import { Alert, Form, Input, InputNumber, Modal, Space, Tag, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { operationApi, JointContractRow } from '@/api/operation'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + +const ROLE_LABEL: Record = { LEAD: '주설계사', SUB: '공동설계사' }; +const ROLE_COLOR: Record = { LEAD: 'blue', SUB: 'default' }; + +const COLS: ColDef[] = [ + { field: 'contractNo', headerName: '계약번호', width: 160 }, + { field: 'agentName', headerName: '설계사', width: 130 }, + { field: 'role', headerName: '역할', width: 120, + cellRenderer: (p: { value: string }) => ( + {ROLE_LABEL[p.value] ?? p.value} + ) }, + { field: 'shareRate', headerName: '지분율(%)', width: 120, type: 'numericColumn', + valueFormatter: (p) => `${((p.value as number) * 100).toFixed(2)}%` }, + { field: 'createdAt', headerName: '등록일', flex: 1 }, +]; + +export default function JointContract() { + const qc = useQueryClient(); + const [searchParams, setSearchParams] = useState<{ contractNo?: string; agentName?: string }>({}); + const [modalOpen, setModalOpen] = useState(false); + const [editingRow, setEditingRow] = useState(null); + const [form] = Form.useForm(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['joint-contract', 'list', searchParams], + queryFn: () => operationApi.jointContractList(searchParams), + retry: false, + }); + + const rows = data?.list ?? []; + + const openCreate = () => { setEditingRow(null); form.resetFields(); setModalOpen(true); }; + const openEdit = (row: JointContractRow) => { setEditingRow(row); form.setFieldsValue({ ...row, shareRate: (row.shareRate as number) * 100 }); setModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + // shareRate: UI는 % 단위 → 백엔드는 0~1 + const body = { ...values, shareRate: (values.shareRate as number) / 100 }; + try { + if (editingRow) { + await operationApi.jointContractUpdate(editingRow.jointId, body); + message.success('수정 완료'); + } else { + await operationApi.jointContractCreate(body); + message.success('등록 완료'); + } + qc.invalidateQueries({ queryKey: ['joint-contract', 'list'] }); + setModalOpen(false); + } catch (err: unknown) { + // E411: 지분율 합계 초과 + const e = err as { code?: string; message?: string }; + if (e?.code === 'E411') { + message.error(e.message ?? '지분율 합계가 1을 초과합니다'); + } + } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '공동계약 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await operationApi.jointContractDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['joint-contract', 'list'] }); + }, + }); + + const actionCol: ColDef = { + headerName: '액션', width: 160, pinned: 'right', + cellRenderer: (p: { data: JointContractRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.jointId)}>삭제 + + ), + }; + + return ( + + {isError && ( + + )} + + setSearchParams({ contractNo: v.contractNo as string, agentName: v.agentName as string })} + onReset={() => setSearchParams({})} + /> + + +
+ + + 공동계약 등록 + +
+ + rows={rows} + columns={[...COLS, actionCol]} + loading={isLoading} + height={520} + rowKey="jointId" + /> +
+ + setModalOpen(false)} + okText="저장" + cancelText="취소" + > +
+ + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/org/GradeEvaluation.tsx b/ga-frontend/src/pages/org/GradeEvaluation.tsx new file mode 100644 index 0000000..5549ad0 --- /dev/null +++ b/ga-frontend/src/pages/org/GradeEvaluation.tsx @@ -0,0 +1,235 @@ +import { useState } from 'react'; +import { Alert, Form, Input, InputNumber, Modal, Space, Tag, Tabs, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { operationApi, GradeEvalRuleRow, GradeEvalRow } from '@/api/operation'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + +const RULE_COLS: ColDef[] = [ + { field: 'ruleId', headerName: 'ID', width: 80, type: 'numericColumn' }, + { field: 'gradeName', headerName: '등급', width: 120 }, + { field: 'minScore', headerName: '최소점수', width: 120, type: 'numericColumn' }, + { field: 'effectiveFrom', headerName: '적용시작', width: 120 }, + { field: 'effectiveTo', headerName: '적용종료', width: 120 }, +]; + +const EVAL_COLS: ColDef[] = [ + { field: 'evalId', headerName: 'ID', width: 80, type: 'numericColumn' }, + { field: 'agentName', headerName: '설계사', width: 130 }, + { field: 'evalPeriod', headerName: '평가기준월', width: 120 }, + { field: 'performanceScore', headerName: '실적점수', width: 110, type: 'numericColumn' }, + { field: 'prevGradeName', headerName: '기존등급', width: 110 }, + { field: 'evaluatedGradeName', headerName: '산정등급', width: 110 }, + { field: 'status', headerName: '상태', width: 110, + cellRenderer: (p: { value: string }) => ( + {p.value === 'APPLIED' ? '반영완료' : '평가완료'} + ) }, +]; + +export default function GradeEvaluation() { + const qc = useQueryClient(); + const [evalParams, setEvalParams] = useState<{ agentName?: string; evalPeriod?: string }>({}); + const [ruleModalOpen, setRuleModalOpen] = useState(false); + const [evalModalOpen, setEvalModalOpen] = useState(false); + const [editingRule, setEditingRule] = useState(null); + const [ruleForm] = Form.useForm(); + const [evalForm] = Form.useForm(); + + const { data: rulesData, isLoading: rulesLoading, isError } = useQuery({ + queryKey: ['grade-eval', 'rules'], + queryFn: () => operationApi.gradeEvalRules(), + retry: false, + }); + + const { data: evalsData, isLoading: evalsLoading } = useQuery({ + queryKey: ['grade-eval', 'list', evalParams], + queryFn: () => operationApi.gradeEvalList(evalParams), + retry: false, + }); + + const rules = rulesData ?? []; + const evals = evalsData?.list ?? []; + + const openRuleCreate = () => { setEditingRule(null); ruleForm.resetFields(); setRuleModalOpen(true); }; + const openRuleEdit = (row: GradeEvalRuleRow) => { setEditingRule(row); ruleForm.setFieldsValue(row); setRuleModalOpen(true); }; + + const handleRuleSave = async () => { + const values = await ruleForm.validateFields(); + try { + if (editingRule) { + await operationApi.gradeEvalRuleUpdate(editingRule.ruleId, values); + message.success('수정 완료'); + } else { + await operationApi.gradeEvalRuleCreate(values); + message.success('등록 완료'); + } + qc.invalidateQueries({ queryKey: ['grade-eval', 'rules'] }); + setRuleModalOpen(false); + } catch { /* request.ts 처리 */ } + }; + + const handleRuleDelete = (id: number) => Modal.confirm({ + title: '룰 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await operationApi.gradeEvalRuleDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['grade-eval', 'rules'] }); + }, + }); + + const handleEvalCreate = async () => { + const values = await evalForm.validateFields(); + try { + await operationApi.gradeEvalCreate(values); + message.success('평가 등록 완료'); + qc.invalidateQueries({ queryKey: ['grade-eval', 'list'] }); + setEvalModalOpen(false); + evalForm.resetFields(); + } catch { /* request.ts 처리 */ } + }; + + const handleApply = (evalId: number) => Modal.confirm({ + title: '등급 반영', content: '산정 등급을 설계사에게 반영하시겠습니까?', + onOk: async () => { + await operationApi.gradeEvalApply(evalId); + message.success('반영 완료'); + qc.invalidateQueries({ queryKey: ['grade-eval', 'list'] }); + }, + }); + + const handleEvalDelete = (evalId: number) => Modal.confirm({ + title: '평가 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await operationApi.gradeEvalDelete(evalId); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['grade-eval', 'list'] }); + }, + }); + + const ruleActionCol: ColDef = { + headerName: '액션', width: 170, pinned: 'right', + cellRenderer: (p: { data: GradeEvalRuleRow }) => ( + + openRuleEdit(p.data)}>수정 + handleRuleDelete(p.data.ruleId)}>삭제 + + ), + }; + + const evalActionCol: ColDef = { + headerName: '액션', width: 190, pinned: 'right', + cellRenderer: (p: { data: GradeEvalRow }) => ( + + {p.data.status === 'EVALUATED' && ( + handleApply(p.data.evalId)}>반영 + )} + handleEvalDelete(p.data.evalId)}>삭제 + + ), + }; + + return ( + + {isError && ( + + )} + + +
+ + + 룰 등록 + +
+ + rows={rules} + columns={[...RULE_COLS, ruleActionCol]} + loading={rulesLoading} + height={520} + rowKey="ruleId" + /> + + ), + }, + { + key: 'evals', + label: '평가 목록', + children: ( + <> + setEvalParams({ agentName: v.agentName as string, evalPeriod: v.evalPeriod as string })} + onReset={() => setEvalParams({})} + /> + +
+ { evalForm.resetFields(); setEvalModalOpen(true); }}> + + 평가 등록 + +
+ + rows={evals} + columns={[...EVAL_COLS, evalActionCol]} + loading={evalsLoading} + height={520} + rowKey="evalId" + /> +
+ + ), + }, + ]} + /> + + {/* 룰 등록/수정 모달 */} + setRuleModalOpen(false)} okText="저장" cancelText="취소"> +
+ + + + + + + + + + + + +
+
+ + {/* 평가 등록 모달 */} + setEvalModalOpen(false)} okText="저장" cancelText="취소"> +
+ + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/org/GuaranteeInsurance.tsx b/ga-frontend/src/pages/org/GuaranteeInsurance.tsx new file mode 100644 index 0000000..796991f --- /dev/null +++ b/ga-frontend/src/pages/org/GuaranteeInsurance.tsx @@ -0,0 +1,148 @@ +import { useState } from 'react'; +import { Alert, Form, Input, InputNumber, Modal, Space, Tag, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { operationApi, GuaranteeInsuranceRow } from '@/api/operation'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; +const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + +const STATUS_COLOR: Record = { ACTIVE: 'green', EXPIRED: 'default', CANCELLED: 'red' }; +const STATUS_LABEL: Record = { ACTIVE: '유효', EXPIRED: '만료', CANCELLED: '해지' }; + +const COLS: ColDef[] = [ + { field: 'agentName', headerName: '설계사', width: 130 }, + { field: 'policyNo', headerName: '증권번호', width: 160 }, + { field: 'carrierName', headerName: '보증보험사', width: 130 }, + { field: 'guaranteeAmount', headerName: '보증금액', width: 150, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'startDate', headerName: '시작일', width: 110 }, + { field: 'endDate', headerName: '종료일', width: 110 }, + { field: 'status', headerName: '상태', width: 100, + cellRenderer: (p: { value: string }) => ( + {STATUS_LABEL[p.value] ?? p.value} + ) }, +]; + +export default function GuaranteeInsurance() { + const qc = useQueryClient(); + const [searchParams, setSearchParams] = useState<{ agentName?: string; status?: string }>({}); + const [modalOpen, setModalOpen] = useState(false); + const [editingRow, setEditingRow] = useState(null); + const [form] = Form.useForm(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['guarantee-insurance', 'list', searchParams], + queryFn: () => operationApi.guaranteeInsuranceList(searchParams), + retry: false, + }); + + const rows = data?.list ?? []; + + const openCreate = () => { setEditingRow(null); form.resetFields(); setModalOpen(true); }; + const openEdit = (row: GuaranteeInsuranceRow) => { setEditingRow(row); form.setFieldsValue(row); setModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + try { + if (editingRow) { + await operationApi.guaranteeInsuranceUpdate(editingRow.guaranteeId, values); + message.success('수정 완료'); + } else { + await operationApi.guaranteeInsuranceCreate(values); + message.success('등록 완료'); + } + qc.invalidateQueries({ queryKey: ['guarantee-insurance', 'list'] }); + setModalOpen(false); + } catch { /* request.ts 처리 */ } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '보증보험 삭제', content: '삭제하시겠습니까?', + onOk: async () => { + await operationApi.guaranteeInsuranceDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['guarantee-insurance', 'list'] }); + }, + }); + + const actionCol: ColDef = { + headerName: '액션', width: 160, pinned: 'right', + cellRenderer: (p: { data: GuaranteeInsuranceRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.guaranteeId)}>삭제 + + ), + }; + + return ( + + {isError && ( + + )} + + setSearchParams({ agentName: v.agentName as string, status: v.status as string })} + onReset={() => setSearchParams({})} + /> + + +
+ + + 보증보험 등록 + +
+ + rows={rows} + columns={[...COLS, actionCol]} + loading={isLoading} + height={520} + rowKey="guaranteeId" + /> +
+ + setModalOpen(false)} + okText="저장" + cancelText="취소" + > +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/settle/HqSettle.tsx b/ga-frontend/src/pages/settle/HqSettle.tsx new file mode 100644 index 0000000..4609db6 --- /dev/null +++ b/ga-frontend/src/pages/settle/HqSettle.tsx @@ -0,0 +1,189 @@ +import { useState } from 'react'; +import { Alert, Col, Form, Input, InputNumber, Modal, Row, Space, Statistic, Tabs, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { operationApi, HqSettleRow } from '@/api/operation'; +import { GRAY, RADIUS, SHADOW, COLOR_SUCCESS, COLOR_ERROR } from '@/theme/tokens'; + +const fmt = (v: unknown) => (v as number)?.toLocaleString() ?? '-'; +const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + +const COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110 }, + { field: 'itemType', headerName: '항목유형', width: 100, + valueFormatter: (p) => p.value === 'INCOME' ? '수입' : '비용' }, + { field: 'itemName', headerName: '항목명', flex: 1 }, + { field: 'amount', headerName: '금액', width: 160, type: 'numericColumn', valueFormatter: (p) => fmt(p.value) }, + { field: 'memo', headerName: '메모', flex: 1 }, + { field: 'status', headerName: '상태', width: 100 }, +]; + +export default function HqSettle() { + const qc = useQueryClient(); + const [settleMonth, setSettleMonth] = useState(dayjs().format('YYYYMM')); + const [modalOpen, setModalOpen] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [form] = Form.useForm(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['hq-settle', 'list', settleMonth], + queryFn: () => operationApi.hqSettleList({ settleMonth }), + retry: false, + }); + + const { data: summary } = useQuery({ + queryKey: ['hq-settle', 'summary', settleMonth], + queryFn: () => operationApi.hqSettleSummary(settleMonth), + retry: false, + enabled: !!settleMonth, + }); + + const rows = data?.list ?? []; + + const openCreate = () => { setEditingItem(null); form.resetFields(); setModalOpen(true); }; + const openEdit = (row: HqSettleRow) => { setEditingItem(row); form.setFieldsValue(row); setModalOpen(true); }; + + const handleSave = async () => { + const values = await form.validateFields(); + try { + if (editingItem) { + await operationApi.hqSettleUpdate(editingItem.itemId, values); + message.success('수정 완료'); + } else { + await operationApi.hqSettleCreate(values); + message.success('등록 완료'); + } + qc.invalidateQueries({ queryKey: ['hq-settle'] }); + setModalOpen(false); + } catch { /* request.ts 처리 */ } + }; + + const handleDelete = (id: number) => Modal.confirm({ + title: '항목 삭제', + content: '삭제하시겠습니까?', + onOk: async () => { + await operationApi.hqSettleDelete(id); + message.success('삭제 완료'); + qc.invalidateQueries({ queryKey: ['hq-settle'] }); + }, + }); + + const actionCol: ColDef = { + headerName: '액션', + width: 160, + pinned: 'right', + cellRenderer: (p: { data: HqSettleRow }) => ( + + openEdit(p.data)}>수정 + handleDelete(p.data.itemId)}>삭제 + + ), + }; + + return ( + + {isError && ( + + )} + + setSettleMonth(v.settleMonth as string ?? dayjs().format('YYYYMM'))} + onReset={() => setSettleMonth(dayjs().format('YYYYMM'))} + /> + + {/* 손익 요약 카드 */} + {summary && ( + + + + (v as number).toLocaleString()} suffix="원" valueStyle={{ color: COLOR_SUCCESS }} /> + + + + + (v as number).toLocaleString()} suffix="원" valueStyle={{ color: COLOR_ERROR }} /> + + + + + (v as number).toLocaleString()} + suffix="원" + valueStyle={{ color: summary.profitLoss >= 0 ? COLOR_SUCCESS : COLOR_ERROR }} + /> + + + + )} + + +
+ + + 항목 등록 + +
+ + rows={rows} + columns={[...COLS, actionCol]} + loading={isLoading} + height={520} + rowKey="itemId" + pinnedBottomRow={rows.length > 0 ? { + itemName: '합계', + amount: rows.reduce((s, r) => s + (r.amount ?? 0), 0), + } as Partial : undefined} + /> + + ), + }, + ]} + /> + + setModalOpen(false)} + okText="저장" + cancelText="취소" + > +
+ + + + + + + + + + + + + + + +
+
+
+ ); +} diff --git a/ga-frontend/src/pages/settle/StepClose.tsx b/ga-frontend/src/pages/settle/StepClose.tsx new file mode 100644 index 0000000..125804a --- /dev/null +++ b/ga-frontend/src/pages/settle/StepClose.tsx @@ -0,0 +1,154 @@ +import { useState } from 'react'; +import { Alert, Button, Form, Input, Modal, Space, Tag, message } from 'antd'; +import { ProCard } from '@ant-design/pro-components'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { ColDef } from '@ag-grid-community/core'; +import dayjs from 'dayjs'; +import PageContainer from '@/components/common/PageContainer'; +import SearchForm from '@/components/common/SearchForm'; +import DataGrid from '@/components/common/DataGrid'; +import PermissionButton from '@/components/common/PermissionButton'; +import { operationApi, StepCloseRow } from '@/api/operation'; +import { GRAY, RADIUS, SHADOW } from '@/theme/tokens'; + +const cardStyle = { background: '#fff', borderRadius: RADIUS.lg, border: `1px solid ${GRAY[100]}`, boxShadow: SHADOW.sm }; + +const STATUS_COLOR: Record = { OPEN: 'green', CLOSED: 'red' }; +const STATUS_LABEL: Record = { OPEN: '열림', CLOSED: '마감' }; +const STEP_LABEL: Record = { RECEIVE: '수신마감', CALC: '계산마감', VERIFY: '검증마감', PAY: '지급마감' }; + +const COLS: ColDef[] = [ + { field: 'settleMonth', headerName: '정산월', width: 110 }, + { field: 'stepSeq', headerName: '순서', width: 70, type: 'numericColumn' }, + { field: 'stepCode', headerName: '단계코드', width: 120, + valueFormatter: (p) => STEP_LABEL[p.value as string] ?? p.value }, + { field: 'status', headerName: '상태', width: 100, + cellRenderer: (p: { value: string }) => ( + {STATUS_LABEL[p.value] ?? p.value} + ) }, + { field: 'closedAt', headerName: '마감일시', flex: 1 }, +]; + +export default function StepClose() { + const qc = useQueryClient(); + const [searchParams, setSearchParams] = useState<{ settleMonth?: string }>({ + settleMonth: dayjs().format('YYYYMM'), + }); + const [createOpen, setCreateOpen] = useState(false); + const [form] = Form.useForm(); + + const { data, isLoading, isError } = useQuery({ + queryKey: ['step-close', 'list', searchParams], + queryFn: () => operationApi.stepCloseList(searchParams), + retry: false, + }); + + const rows = data?.list ?? []; + + const handleClose = (id: number) => Modal.confirm({ + title: '단계 마감', + content: '해당 단계를 마감하시겠습니까?', + onOk: async () => { + await operationApi.stepCloseClose(id); + message.success('마감 완료'); + qc.invalidateQueries({ queryKey: ['step-close', 'list'] }); + }, + }); + + const handleReopen = (id: number) => Modal.confirm({ + title: '단계 재오픈', + content: '해당 단계를 재오픈하시겠습니까?', + onOk: async () => { + await operationApi.stepCloseReopen(id); + message.success('재오픈 완료'); + qc.invalidateQueries({ queryKey: ['step-close', 'list'] }); + }, + }); + + const handleCreate = async () => { + const values = await form.validateFields(); + try { + await operationApi.stepCloseCreate(values); + message.success('등록 완료'); + qc.invalidateQueries({ queryKey: ['step-close', 'list'] }); + setCreateOpen(false); + form.resetFields(); + } catch { /* request.ts 처리 */ } + }; + + const actionCol: ColDef = { + headerName: '액션', + width: 160, + pinned: 'right', + cellRenderer: (p: { data: StepCloseRow }) => ( + + {p.data.status === 'OPEN' ? ( + handleClose(p.data.stepCloseId)} + > + 마감 + + ) : ( + handleReopen(p.data.stepCloseId)} + > + 재오픈 + + )} + + ), + }; + + return ( + + {isError && ( + + )} + + setSearchParams({ settleMonth: v.settleMonth as string })} + onReset={() => setSearchParams({ settleMonth: dayjs().format('YYYYMM') })} + /> + + +
+ setCreateOpen(true)}> + + 단계 등록 + +
+ + rows={rows} + columns={[...COLS, actionCol]} + loading={isLoading} + height={520} + rowKey="stepCloseId" + /> +
+ + { setCreateOpen(false); form.resetFields(); }} okText="저장" cancelText="취소"> +
+ + + + + + + + + +
+
+
+ ); +}