feat: 수수료 공식 갭 보완 + 8종 신규모듈 풀스택 (V116~V123)

수수료 공식 갭분석(5 병렬감사) 후 확정 버그 3건 수정 + 개념적으로 빠진 모듈 8종 풀스택 구현.

[확정 버그 3건]
- 1200% 누적한도(checkContractTotal)를 배치 CalcRecruitStep에 연결(기존 1차년 한도만 적용
  → 두 한도 중 더 제한적인 값으로 clip+이연). +CalcRecruitStepTest
- BatchInstallmentPlanGenerator planMonth 반영(하드코딩 1 제거, API판과 동일). +단위테스트
- PersistencyBonus 임계유지율 게이트 + FIXED 정액 룰 도출. +PersistencyBonusServiceTest

[신규 모듈 8종]
- MOD-1 도입수수료(recruiter/development) / MOD-2 선지급·차익정산 / MOD-3 환수 분할상환
- MOD-4 익월부활 재지급 / MOD-5 정산대사 입수 / MOD-6 환수 시효 / MOD-7 환수 감면
- MOD-8 채널별 수수료차등(sales_channel)
각 DB(테이블+메뉴+권한+공통코드)→VO/Mapper/Enum→Service/Controller→화면 풀스택.
배치 정산연계: AggregateStep에 도입/익월부활 가산(11종), 선지급 FIFO 상계(멱등 reverse),
MOD-8 채널 우선 요율조회(NULL 폴백 하위호환).

[검증]
- 전체 ./gradlew build(전 모듈+전 테스트) GREEN
- Flyway V116~V123 운영DB 적용(schema v115→v123)
- 라이브 스모크 8 GET=200, MOD-1 RATE공식 50000 실측, 액션 POST 5xx 0건
- 적대적 코드리뷰 APPROVED(MOD-8 계산기 미연결 버그 발견·수정)

스펙: docs/DOMAIN_GAP_MOD1-8.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
GA Pro
2026-06-06 17:32:25 +09:00
parent 00ead64c4c
commit ff6d399dd3
114 changed files with 6419 additions and 53 deletions
@@ -0,0 +1,39 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.AdvancePaymentResp;
import com.ga.core.vo.commission.AdvancePaymentSearchParam;
import com.ga.core.vo.commission.AdvancePaymentVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
/**
* advance_payment Mapper — 선지급금 마스터 (V117)
*/
@Mapper
public interface AdvancePaymentMapper {
AdvancePaymentResp selectById(@Param("advanceId") Long advanceId);
List<AdvancePaymentResp> selectList(AdvancePaymentSearchParam param);
int insert(AdvancePaymentVO vo);
int update(AdvancePaymentVO vo);
int delete(@Param("advanceId") Long advanceId);
/**
* 설계사의 미상환 잔액 합계.
* status IN ('OUTSTANDING','PARTIAL') 대상 SUM(balance).
*/
BigDecimal outstandingByAgent(@Param("agentId") Long agentId);
/**
* 설계사의 FIFO 상계 대상 선지급금 목록.
* status IN ('OUTSTANDING','PARTIAL') ORDER BY advance_month ASC, advance_id ASC.
*/
List<AdvancePaymentVO> selectOutstandingFifo(@Param("agentId") Long agentId);
}
@@ -0,0 +1,34 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.AdvanceRecoveryLedgerVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* advance_recovery_ledger Mapper — 선지급 상계 이력 (V117)
*/
@Mapper
public interface AdvanceRecoveryLedgerMapper {
int insert(AdvanceRecoveryLedgerVO vo);
/**
* 배치 멱등용: 당월 상계 이력 삭제 (역산 후 재적용 패턴).
*/
int deleteBySettleMonth(@Param("advanceId") Long advanceId,
@Param("settleMonth") String settleMonth);
/**
* 선지급금 ID + 정산월 기준 상계 이력 조회.
*/
List<AdvanceRecoveryLedgerVO> selectBySettleMonth(@Param("advanceId") Long advanceId,
@Param("settleMonth") String settleMonth);
/**
* 배치 멱등용: 해당 정산월에 상계 이력이 있는 선지급금 ID 목록(중복 제거).
* reverse 단계에서 월 단위로 역산 대상을 식별한다.
*/
List<Long> selectAdvanceIdsBySettleMonth(@Param("settleMonth") String settleMonth);
}
@@ -0,0 +1,29 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.CarrierReconciliationInResp;
import com.ga.core.vo.commission.CarrierReconciliationInSearchParam;
import com.ga.core.vo.commission.CarrierReconciliationInVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* carrier_reconciliation_in Mapper — 보험사 정산대사 입수 마스터 (V120)
*/
@Mapper
public interface CarrierReconciliationInMapper {
CarrierReconciliationInResp selectById(@Param("reconId") Long reconId);
List<CarrierReconciliationInResp> selectList(CarrierReconciliationInSearchParam param);
int insert(CarrierReconciliationInVO vo);
int update(CarrierReconciliationInVO vo);
/**
* 보험사 + 정산월 기준 upsert — UNIQUE(company_id, settle_month) 충돌 시 갱신.
*/
int upsert(CarrierReconciliationInVO vo);
}
@@ -0,0 +1,31 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.ChargebackWaiverRequestResp;
import com.ga.core.vo.commission.ChargebackWaiverRequestSearchParam;
import com.ga.core.vo.commission.ChargebackWaiverRequestVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List;
/**
* chargeback_waiver_request Mapper — 환수 감면 신청 (V122)
*/
@Mapper
public interface ChargebackWaiverRequestMapper {
ChargebackWaiverRequestResp selectById(@Param("requestId") Long requestId);
List<ChargebackWaiverRequestResp> selectList(ChargebackWaiverRequestSearchParam param);
int insert(ChargebackWaiverRequestVO vo);
/**
* 승인/반려 처리 — approval_status, approver_id, approved_at, updated_at 갱신.
*/
int updateApproval(@Param("requestId") Long requestId,
@Param("approvalStatus") String approvalStatus,
@Param("approverId") Long approverId,
@Param("approvedAt") LocalDateTime approvedAt);
}
@@ -0,0 +1,36 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.ClawbackInstallmentPlanResp;
import com.ga.core.vo.commission.ClawbackInstallmentPlanSearchParam;
import com.ga.core.vo.commission.ClawbackInstallmentPlanVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* clawback_installment_plan Mapper — 환수 분할상환 계획 (V118)
*/
@Mapper
public interface ClawbackInstallmentPlanMapper {
ClawbackInstallmentPlanResp selectById(@Param("planId") Long planId);
List<ClawbackInstallmentPlanResp> selectList(ClawbackInstallmentPlanSearchParam param);
/**
* 채권 ID 기준 분할상환 계획 목록 — 회차순 정렬.
*/
List<ClawbackInstallmentPlanVO> selectByReceivable(@Param("receivableId") Long receivableId);
/**
* 분할상환 계획 일괄 등록.
*/
int insertBatch(List<ClawbackInstallmentPlanVO> plans);
/**
* 분할상환 회차 상태 갱신 (SCHEDULED→RECOVERED 또는 CANCELLED).
*/
int updateStatus(@Param("planId") Long planId,
@Param("status") String status);
}
@@ -77,4 +77,20 @@ public interface ClawbackReceivableMapper {
List<Map<String, Object>> summaryByAgent(ClawbackReceivableSearchParam param);
int countByCondition(ClawbackReceivableSearchParam param);
/**
* [MOD-6] 시효 만료 대상 채권 조회.
* origin_settle_month <= beforeMonth AND status IN('OUTSTANDING','RECOVERING')
* beforeMonth = baseMonth - expiration_months (Service에서 계산 후 전달).
*/
List<ClawbackReceivableVO> selectExpired(@Param("beforeMonth") String beforeMonth);
/**
* [MOD-6] 만료 대상 채권 일괄 대손 처리.
* status='WRITTEN_OFF', writeoff_reason=reason, written_off_at=NOW(), updated_at=NOW()
* 대상: origin_settle_month <= beforeMonth AND status IN('OUTSTANDING','RECOVERING')
* 처리 건수 반환.
*/
int writeOffExpired(@Param("beforeMonth") String beforeMonth,
@Param("reason") String reason);
}
@@ -0,0 +1,33 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.LapsedReinstatementRecoveryResp;
import com.ga.core.vo.commission.LapsedReinstatementRecoverySearchParam;
import com.ga.core.vo.commission.LapsedReinstatementRecoveryVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* lapsed_reinstatement_recovery Mapper — 익월 부활 재지급 원장 (V119)
*/
@Mapper
public interface LapsedReinstatementRecoveryMapper {
LapsedReinstatementRecoveryResp selectById(@Param("recoveryId") Long recoveryId);
List<LapsedReinstatementRecoveryResp> selectList(LapsedReinstatementRecoverySearchParam param);
int insert(LapsedReinstatementRecoveryVO vo);
int updateStatus(@Param("recoveryId") Long recoveryId,
@Param("status") String status,
@Param("repaidAmount") java.math.BigDecimal repaidAmount);
/**
* 정산월 기준 설계사별 재지급 합계 — AggregateStep other_commission_total 가산용.
* status='REPAID' 대상 SUM(repaid_amount) GROUP BY agent_id.
*/
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
}
@@ -0,0 +1,24 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.ReconciliationVarianceVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* reconciliation_variance Mapper — 대사 차이 상세 (V120)
*/
@Mapper
public interface ReconciliationVarianceMapper {
/**
* 차이 상세 일괄 등록.
*/
int insertBatch(List<ReconciliationVarianceVO> variances);
/**
* 대사 입수 ID 기준 차이 상세 목록 조회.
*/
List<ReconciliationVarianceVO> selectByRecon(@Param("reconId") Long reconId);
}
@@ -0,0 +1,33 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.RecruiterCommissionLedgerResp;
import com.ga.core.vo.commission.RecruiterCommissionLedgerSearchParam;
import com.ga.core.vo.commission.RecruiterCommissionLedgerVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* recruiter_commission_ledger Mapper — 도입수수료 원장 (V116)
*/
@Mapper
public interface RecruiterCommissionLedgerMapper {
RecruiterCommissionLedgerResp selectById(@Param("ledgerId") Long ledgerId);
List<RecruiterCommissionLedgerResp> selectList(RecruiterCommissionLedgerSearchParam param);
int insert(RecruiterCommissionLedgerVO vo);
int updateStatus(@Param("ledgerId") Long ledgerId, @Param("status") String status);
int delete(@Param("ledgerId") Long ledgerId);
/**
* 정산월 기준 도입자 설계사별 도입수수료 합계 — AggregateStep other_commission_total 가산용.
* recruiter_agent_id 기준으로 agentId alias, CANCELLED 제외.
*/
List<Map<String, Object>> aggregateByAgent(@Param("settleMonth") String settleMonth);
}
@@ -0,0 +1,26 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.RecruiterCommissionRuleResp;
import com.ga.core.vo.commission.RecruiterCommissionRuleSearchParam;
import com.ga.core.vo.commission.RecruiterCommissionRuleVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* recruiter_commission_rule Mapper — 도입수수료 룰 마스터 (V116)
*/
@Mapper
public interface RecruiterCommissionRuleMapper {
RecruiterCommissionRuleResp selectById(@Param("ruleId") Long ruleId);
List<RecruiterCommissionRuleResp> selectList(RecruiterCommissionRuleSearchParam param);
int insert(RecruiterCommissionRuleVO vo);
int update(RecruiterCommissionRuleVO vo);
int delete(@Param("ruleId") Long ruleId);
}
@@ -0,0 +1,28 @@
package com.ga.core.mapper.commission;
import com.ga.core.vo.commission.StatuteConfigResp;
import com.ga.core.vo.commission.StatuteConfigVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* statute_config Mapper — 환수 시효 설정 (V121)
*/
@Mapper
public interface StatuteConfigMapper {
StatuteConfigResp selectById(@Param("configId") Long configId);
/**
* entity_type 기준 단건 조회 (UNIQUE).
*/
StatuteConfigResp selectByEntityType(@Param("entityType") String entityType);
List<StatuteConfigResp> selectList();
int insert(StatuteConfigVO vo);
int update(StatuteConfigVO vo);
}
@@ -17,10 +17,11 @@ public interface RuleMapper {
// === commission_rate ===
List<CommissionRateVO> selectCommissionRates(@Param("productId") Long productId,
@Param("year") Integer year);
/** 계약일에 유효한 보험사 수수료율 */
/** 계약일에 유효한 보험사 수수료율 (판매채널 우선, NULL=모든채널 폴백) */
BigDecimal findCompanyRate(@Param("productId") Long productId,
@Param("year") Integer year,
@Param("baseDate") LocalDate baseDate);
@Param("baseDate") LocalDate baseDate,
@Param("salesChannel") String salesChannel);
int insertCommissionRate(CommissionRateVO vo);
int updateCommissionRate(CommissionRateVO vo);
int deleteCommissionRate(@Param("rateId") Long rateId);
@@ -31,7 +32,8 @@ public interface RuleMapper {
BigDecimal findPayoutRate(@Param("gradeId") Long gradeId,
@Param("insuranceType") String insuranceType,
@Param("year") Integer year,
@Param("baseDate") LocalDate baseDate);
@Param("baseDate") LocalDate baseDate,
@Param("salesChannel") String salesChannel);
int insertPayoutRule(PayoutRuleVO vo);
int updatePayoutRule(PayoutRuleVO vo);
int deletePayoutRule(@Param("ruleId") Long ruleId);
@@ -0,0 +1,28 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* advance_payment 조회 응답 — 설계사명 조인 포함 (V117)
*/
@Data
public class AdvancePaymentResp {
private Long advanceId;
private Long agentId;
/** 설계사명 */
private String agentName;
/** 조직명 */
private String orgName;
private String advanceMonth;
private BigDecimal principalAmount;
private BigDecimal recoveredAmount;
private BigDecimal balance;
private String status;
/** 공통코드 ADVANCE_STATUS */
private String statusName;
private String memo;
private LocalDateTime createdAt;
}
@@ -0,0 +1,27 @@
package com.ga.core.vo.commission;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* advance_payment 등록 요청 (V117)
*/
@Data
public class AdvancePaymentSaveReq {
@NotNull
private Long agentId;
@NotBlank
private String advanceMonth;
@NotNull
@DecimalMin(value = "0", inclusive = false)
private BigDecimal principalAmount;
private String memo;
}
@@ -0,0 +1,17 @@
package com.ga.core.vo.commission;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* advance_payment 검색 조건 (V117)
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class AdvancePaymentSearchParam extends SearchParam {
private Long agentId;
private String agentName;
private String advanceMonth;
private String status;
}
@@ -0,0 +1,29 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* advance_payment — 선지급금 마스터 (V117)
*/
@Data
public class AdvancePaymentVO {
private Long advanceId;
private Long agentId;
/** 선지급 발생월 YYYYMM */
private String advanceMonth;
/** 원금 */
private BigDecimal principalAmount;
/** 회수 누계 */
private BigDecimal recoveredAmount;
/** 잔액 (principal - recovered) */
private BigDecimal balance;
/** 상태: OUTSTANDING/PARTIAL/CLEARED/WAIVED */
private String status;
private String memo;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,21 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* advance_recovery_ledger — 선지급 상계 이력 (V117)
*/
@Data
public class AdvanceRecoveryLedgerVO {
private Long recoveryId;
private Long advanceId;
/** 상계 정산월 YYYYMM */
private String settleMonth;
/** 당월 상계 금액 */
private BigDecimal recoveryAmount;
private LocalDateTime createdAt;
private Long createdBy;
}
@@ -0,0 +1,28 @@
package com.ga.core.vo.commission;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* 선지급 상태 (ADVANCE_STATUS 그룹, V117)
*/
public enum AdvanceStatus {
OUTSTANDING("미상환"),
PARTIAL("부분상환"),
CLEARED("상환완료"),
WAIVED("면제");
private final String label;
AdvanceStatus(String label) {
this.label = label;
}
@JsonValue
public String getCode() {
return this.name();
}
public String getLabel() {
return label;
}
}
@@ -0,0 +1,29 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* carrier_reconciliation_in 조회 응답 — 보험사명 조인 포함 (V120)
*/
@Data
public class CarrierReconciliationInResp {
private Long reconId;
private Long companyId;
/** 보험사 코드 */
private String companyCode;
/** 보험사명 */
private String companyName;
private String settleMonth;
private String fileName;
private Integer recordCount;
private BigDecimal reportedTotal;
private BigDecimal systemTotal;
private BigDecimal varianceAmount;
private String status;
/** 공통코드 RECON_IN_STATUS */
private String statusName;
private LocalDateTime createdAt;
}
@@ -0,0 +1,29 @@
package com.ga.core.vo.commission;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* carrier_reconciliation_in 등록/수정 요청 (V120)
*/
@Data
public class CarrierReconciliationInSaveReq {
@NotNull
private Long companyId;
@NotBlank
private String settleMonth;
private String fileName;
private Integer recordCount;
@NotNull
@DecimalMin("0")
private BigDecimal reportedTotal;
}
@@ -0,0 +1,16 @@
package com.ga.core.vo.commission;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* carrier_reconciliation_in 검색 조건 (V120)
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class CarrierReconciliationInSearchParam extends SearchParam {
private Long companyId;
private String settleMonth;
private String status;
}
@@ -0,0 +1,30 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* carrier_reconciliation_in — 보험사 정산대사 입수 마스터 (V120)
*/
@Data
public class CarrierReconciliationInVO {
private Long reconId;
private Long companyId;
/** 정산월 YYYYMM */
private String settleMonth;
private String fileName;
private Integer recordCount;
/** 보험사 통보 금액 합계 */
private BigDecimal reportedTotal;
/** GA 시스템 집계 금액 */
private BigDecimal systemTotal;
/** 차이 금액 (reported - system) */
private BigDecimal varianceAmount;
/** 상태: RECEIVED/MATCHED/VARIANCE/APPROVED */
private String status;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,27 @@
package com.ga.core.vo.commission;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* 환수 분할상환 상태 (CB_INSTALLMENT_STATUS 그룹, V118)
*/
public enum CbInstallmentStatus {
SCHEDULED("예정"),
RECOVERED("회수"),
CANCELLED("취소");
private final String label;
CbInstallmentStatus(String label) {
this.label = label;
}
@JsonValue
public String getCode() {
return this.name();
}
public String getLabel() {
return label;
}
}
@@ -0,0 +1,32 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* chargeback_waiver_request 조회 응답 — 설계사명·계약정보 조인 포함 (V122)
*/
@Data
public class ChargebackWaiverRequestResp {
private Long requestId;
private Long contractId;
/** 증권번호 */
private String policyNo;
private Long agentId;
/** 설계사명 */
private String agentName;
/** 조직명 */
private String orgName;
private String settleMonth;
private BigDecimal originalChargeback;
private BigDecimal waiverAmount;
private String reason;
private String approvalStatus;
/** 공통코드 WAIVER_STATUS */
private String approvalStatusName;
private Long approverId;
private LocalDateTime approvedAt;
private LocalDateTime createdAt;
}
@@ -0,0 +1,35 @@
package com.ga.core.vo.commission;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* chargeback_waiver_request 등록 요청 (V122).
* waiver_amount <= original_chargeback 검증은 Service에서 수행.
*/
@Data
public class ChargebackWaiverRequestSaveReq {
@NotNull
private Long contractId;
@NotNull
private Long agentId;
@NotBlank
private String settleMonth;
@NotNull
@DecimalMin("0")
private BigDecimal originalChargeback;
@NotNull
@DecimalMin("0")
private BigDecimal waiverAmount;
private String reason;
}
@@ -0,0 +1,17 @@
package com.ga.core.vo.commission;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* chargeback_waiver_request 검색 조건 (V122)
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ChargebackWaiverRequestSearchParam extends SearchParam {
private Long agentId;
private String agentName;
private String settleMonth;
private String approvalStatus;
}
@@ -0,0 +1,33 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* chargeback_waiver_request — 환수 감면 신청 (V122)
*/
@Data
public class ChargebackWaiverRequestVO {
private Long requestId;
private Long contractId;
private Long agentId;
/** 정산월 YYYYMM */
private String settleMonth;
/** 원래 환수액 */
private BigDecimal originalChargeback;
/** 감면 신청 금액 (<=originalChargeback) */
private BigDecimal waiverAmount;
/** 감면 사유 */
private String reason;
/** 승인 상태: REQUESTED/APPROVED/REJECTED */
private String approvalStatus;
/** 승인자 사용자 ID */
private Long approverId;
/** 승인/반려 일시 */
private LocalDateTime approvedAt;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,28 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* clawback_installment_plan 조회 응답 — 채권 정보 조인 포함 (V118)
*/
@Data
public class ClawbackInstallmentPlanResp {
private Long planId;
private Long receivableId;
/** 채권 발생월 (clawback_receivable.origin_settle_month) */
private String originSettleMonth;
/** 채권 설계사 ID */
private Long agentId;
/** 채권 설계사명 */
private String agentName;
private Integer installmentNo;
private BigDecimal installmentAmount;
private String dueMonth;
private String status;
/** 공통코드 CB_INSTALLMENT_STATUS */
private String statusName;
private LocalDateTime recoveredAt;
}
@@ -0,0 +1,31 @@
package com.ga.core.vo.commission;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* clawback_installment_plan 단건 등록 요청 (V118).
* 일괄 생성은 Service의 generate(receivableId, count) 사용.
*/
@Data
public class ClawbackInstallmentPlanSaveReq {
@NotNull
private Long receivableId;
@NotNull
@Min(1)
private Integer installmentNo;
@NotNull
@DecimalMin(value = "0", inclusive = false)
private BigDecimal installmentAmount;
@NotBlank
private String dueMonth;
}
@@ -0,0 +1,17 @@
package com.ga.core.vo.commission;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* clawback_installment_plan 검색 조건 (V118)
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ClawbackInstallmentPlanSearchParam extends SearchParam {
private Long receivableId;
private Long agentId;
private String dueMonth;
private String status;
}
@@ -0,0 +1,28 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* clawback_installment_plan — 환수 분할상환 계획 (V118)
*/
@Data
public class ClawbackInstallmentPlanVO {
private Long planId;
private Long receivableId;
/** 분할 회차 번호 (1부터) */
private Integer installmentNo;
/** 회차 납부 금액 */
private BigDecimal installmentAmount;
/** 납부 예정월 YYYYMM */
private String dueMonth;
/** 상태: SCHEDULED/RECOVERED/CANCELLED */
private String status;
/** 실제 회수 일시 */
private LocalDateTime recoveredAt;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,27 @@
package com.ga.core.vo.commission;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* 익월 부활 재지급 상태 (LAPSE_RECOVERY_STATUS 그룹, V119)
*/
public enum LapseRecoveryStatus {
PENDING("대기"),
REPAID("재지급완료"),
CANCELLED("취소");
private final String label;
LapseRecoveryStatus(String label) {
this.label = label;
}
@JsonValue
public String getCode() {
return this.name();
}
public String getLabel() {
return label;
}
}
@@ -0,0 +1,28 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
/**
* lapsed_reinstatement_recovery 조회 응답 — 설계사명·계약정보 조인 포함 (V119)
*/
@Data
public class LapsedReinstatementRecoveryResp {
private Long recoveryId;
private Long contractId;
/** 증권번호 */
private String policyNo;
private Long agentId;
/** 설계사명 */
private String agentName;
/** 조직명 */
private String orgName;
private String lapsedMonth;
private String reinstatedMonth;
private BigDecimal withheldAmount;
private BigDecimal repaidAmount;
private String status;
/** 공통코드 LAPSE_RECOVERY_STATUS */
private String statusName;
}
@@ -0,0 +1,31 @@
package com.ga.core.vo.commission;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* lapsed_reinstatement_recovery 등록 요청 (V119)
*/
@Data
public class LapsedReinstatementRecoverySaveReq {
@NotNull
private Long contractId;
@NotNull
private Long agentId;
@NotBlank
private String lapsedMonth;
@NotBlank
private String reinstatedMonth;
@NotNull
@DecimalMin("0")
private BigDecimal withheldAmount;
}
@@ -0,0 +1,18 @@
package com.ga.core.vo.commission;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* lapsed_reinstatement_recovery 검색 조건 (V119)
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class LapsedReinstatementRecoverySearchParam extends SearchParam {
private Long agentId;
private String agentName;
private String reinstatedMonth;
private String lapsedMonth;
private String status;
}
@@ -0,0 +1,29 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* lapsed_reinstatement_recovery — 익월 부활 재지급 원장 (V119)
*/
@Data
public class LapsedReinstatementRecoveryVO {
private Long recoveryId;
private Long contractId;
private Long agentId;
/** 실효 발생월 YYYYMM */
private String lapsedMonth;
/** 부활 확정월 YYYYMM */
private String reinstatedMonth;
/** 보류된 수수료 금액 */
private BigDecimal withheldAmount;
/** 재지급 완료 금액 */
private BigDecimal repaidAmount;
/** 상태: PENDING/REPAID/CANCELLED */
private String status;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,28 @@
package com.ga.core.vo.commission;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* 정산대사 입수 상태 (RECON_IN_STATUS 그룹, V120)
*/
public enum ReconInStatus {
RECEIVED("입수"),
MATCHED("일치"),
VARIANCE("차이"),
APPROVED("승인");
private final String label;
ReconInStatus(String label) {
this.label = label;
}
@JsonValue
public String getCode() {
return this.name();
}
public String getLabel() {
return label;
}
}
@@ -0,0 +1,25 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* reconciliation_variance — 대사 차이 상세 (V120)
*/
@Data
public class ReconciliationVarianceVO {
private Long varianceId;
private Long reconId;
/** 증권번호 */
private String policyNo;
/** GA 시스템 예상 금액 */
private BigDecimal expectedAmount;
/** 보험사 통보 금액 */
private BigDecimal reportedAmount;
/** 차이 사유 */
private String varianceReason;
private LocalDateTime createdAt;
private Long createdBy;
}
@@ -0,0 +1,28 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
/**
* recruiter_commission_ledger 조회 응답 — 설계사명·공통코드명 조인 포함 (V116)
*/
@Data
public class RecruiterCommissionLedgerResp {
private Long ledgerId;
private Long recruiterAgentId;
/** 도입자 설계사명 */
private String recruiterAgentName;
private Long recruitAgentId;
/** 피도입 설계사명 */
private String recruitAgentName;
private String settleMonth;
private String recruiterType;
/** 공통코드 RECRUITER_TYPE */
private String recruiterTypeName;
private BigDecimal baseAmount;
private BigDecimal commissionAmount;
private String status;
/** 공통코드 상태명 */
private String statusName;
}
@@ -0,0 +1,35 @@
package com.ga.core.vo.commission;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
/**
* recruiter_commission_ledger 등록 요청 (V116)
*/
@Data
public class RecruiterCommissionLedgerSaveReq {
@NotNull
private Long recruiterAgentId;
@NotNull
private Long recruitAgentId;
@NotBlank
private String settleMonth;
@NotBlank
private String recruiterType;
@NotNull
@DecimalMin("0")
private BigDecimal baseAmount;
@NotNull
@DecimalMin("0")
private BigDecimal commissionAmount;
}
@@ -0,0 +1,18 @@
package com.ga.core.vo.commission;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* recruiter_commission_ledger 검색 조건 (V116)
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class RecruiterCommissionLedgerSearchParam extends SearchParam {
private Long recruiterAgentId;
private Long recruitAgentId;
private String settleMonth;
private String recruiterType;
private String status;
}
@@ -0,0 +1,28 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* recruiter_commission_ledger — 도입수수료 원장 (V116)
*/
@Data
public class RecruiterCommissionLedgerVO {
private Long ledgerId;
private Long recruiterAgentId;
private Long recruitAgentId;
private String settleMonth;
/** RecruiterType: NEW_RECRUIT/DEVELOPMENT/TRAINING */
private String recruiterType;
/** 기준금액 (피도입 설계사 당월 모집수수료 합) */
private BigDecimal baseAmount;
/** 도입수수료 금액 */
private BigDecimal commissionAmount;
/** 상태: CALCULATED/CONFIRMED/PAID/CANCELLED */
private String status;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,24 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* recruiter_commission_rule 조회 응답 — 공통코드명 조인 포함 (V116)
*/
@Data
public class RecruiterCommissionRuleResp {
private Long ruleId;
private String recruiterType;
/** 공통코드 RECRUITER_TYPE */
private String recruiterTypeName;
private String calcType;
/** 공통코드 CALC_TYPE */
private String calcTypeName;
private BigDecimal commissionRate;
private BigDecimal fixedAmount;
private LocalDate effectiveFrom;
private LocalDate effectiveTo;
}
@@ -0,0 +1,35 @@
package com.ga.core.vo.commission;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
* recruiter_commission_rule 등록/수정 요청 (V116)
*/
@Data
public class RecruiterCommissionRuleSaveReq {
@NotBlank
private String recruiterType;
@NotBlank
private String calcType;
@DecimalMin("0")
@DecimalMax("1")
private BigDecimal commissionRate;
@DecimalMin("0")
private BigDecimal fixedAmount;
@NotNull
private LocalDate effectiveFrom;
private LocalDate effectiveTo;
}
@@ -0,0 +1,15 @@
package com.ga.core.vo.commission;
import com.ga.common.model.SearchParam;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* recruiter_commission_rule 검색 조건 (V116)
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class RecruiterCommissionRuleSearchParam extends SearchParam {
private String recruiterType;
private String calcType;
}
@@ -0,0 +1,28 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* recruiter_commission_rule — 도입수수료 룰 마스터 (V116)
*/
@Data
public class RecruiterCommissionRuleVO {
private Long ruleId;
/** RecruiterType: NEW_RECRUIT/DEVELOPMENT/TRAINING */
private String recruiterType;
/** CalcType: RATE/FIXED */
private String calcType;
/** 정률 요율 (0~1, calc_type=RATE 일 때 사용) */
private BigDecimal commissionRate;
/** 정액 금액 (calc_type=FIXED 일 때 사용) */
private BigDecimal fixedAmount;
private LocalDate effectiveFrom;
private LocalDate effectiveTo;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,27 @@
package com.ga.core.vo.commission;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* 도입수수료 유형 (RECRUITER_TYPE 그룹, V116)
*/
public enum RecruiterType {
NEW_RECRUIT("신인도입"),
DEVELOPMENT("육성"),
TRAINING("교육");
private final String label;
RecruiterType(String label) {
this.label = label;
}
@JsonValue
public String getCode() {
return this.name();
}
public String getLabel() {
return label;
}
}
@@ -0,0 +1,14 @@
package com.ga.core.vo.commission;
import lombok.Data;
/**
* statute_config 조회 응답 (V121)
*/
@Data
public class StatuteConfigResp {
private Long configId;
private String entityType;
private Integer expirationMonths;
private String isActive;
}
@@ -0,0 +1,22 @@
package com.ga.core.vo.commission;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* statute_config 등록/수정 요청 (V121)
*/
@Data
public class StatuteConfigSaveReq {
@NotBlank
private String entityType;
@NotNull
@Min(1)
private Integer expirationMonths;
private String isActive;
}
@@ -0,0 +1,22 @@
package com.ga.core.vo.commission;
import lombok.Data;
import java.time.LocalDateTime;
/**
* statute_config — 환수 시효 설정 (V121)
*/
@Data
public class StatuteConfigVO {
private Long configId;
/** 대상 엔티티 타입 (CLAWBACK_RECEIVABLE) */
private String entityType;
/** 시효 만료 개월수 */
private Integer expirationMonths;
/** 활성 여부 (Y/N) */
private String isActive;
private LocalDateTime createdAt;
private Long createdBy;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,27 @@
package com.ga.core.vo.commission;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* 환수 감면 승인 상태 (WAIVER_STATUS 그룹, V122)
*/
public enum WaiverStatus {
REQUESTED("신청"),
APPROVED("승인"),
REJECTED("반려");
private final String label;
WaiverStatus(String label) {
this.label = label;
}
@JsonValue
public String getCode() {
return this.name();
}
public String getLabel() {
return label;
}
}
@@ -30,4 +30,8 @@ public class ContractResp {
private LocalDate contractDate;
private LocalDate effectiveDate;
private LocalDate lapseDate;
/** 판매채널 (NULL=DIRECT 취급): DIRECT/BANCASSURANCE/HOME_SHOPPING/PARTNERSHIP (V123) */
private String salesChannel;
/** 공통코드 SALES_CHANNEL */
private String salesChannelName;
}
@@ -23,4 +23,6 @@ public class ContractSaveReq {
private Integer coveragePeriod;
@NotNull private LocalDate contractDate;
private LocalDate effectiveDate;
/** 판매채널 (NULL=DIRECT): DIRECT/BANCASSURANCE/HOME_SHOPPING/PARTNERSHIP (V123) */
private String salesChannel;
}
@@ -23,6 +23,8 @@ public class ContractVO {
private LocalDate effectiveDate;
private LocalDate lapseDate;
private LocalDate revivalDate;
/** 판매채널 (NULL=DIRECT 취급): DIRECT/BANCASSURANCE/HOME_SHOPPING/PARTNERSHIP (V123) */
private String salesChannel;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.commission.AdvancePaymentMapper">
<resultMap id="VOMap" type="com.ga.core.vo.commission.AdvancePaymentVO"/>
<resultMap id="RespMap" type="com.ga.core.vo.commission.AdvancePaymentResp"/>
<!-- 목록/단건 조회 컬럼 — 설계사명·조직명·공통코드명 조인 포함 -->
<sql id="advJoinCols">
ap.advance_id,
ap.agent_id, a.agent_name, o.org_name,
ap.advance_month,
ap.principal_amount,
ap.recovered_amount,
ap.balance,
ap.status,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'ADVANCE_STATUS' AND cc.code = ap.status) AS status_name,
ap.memo,
ap.created_at
</sql>
<sql id="advJoins">
FROM advance_payment ap
LEFT JOIN agent a ON a.agent_id = ap.agent_id
LEFT JOIN organization o ON o.org_id = a.org_id
</sql>
<select id="selectById" resultMap="RespMap">
SELECT <include refid="advJoinCols"/>
<include refid="advJoins"/>
WHERE ap.advance_id = #{advanceId}
</select>
<select id="selectList" resultMap="RespMap">
SELECT <include refid="advJoinCols"/>
<include refid="advJoins"/>
<where>
<if test="agentId != null">AND ap.agent_id = #{agentId}</if>
<if test="agentName != null and agentName != ''">AND a.agent_name LIKE '%' || #{agentName} || '%'</if>
<if test="advanceMonth != null and advanceMonth != ''">AND ap.advance_month = #{advanceMonth}</if>
<if test="status != null and status != ''">AND ap.status = #{status}</if>
</where>
ORDER BY ap.advance_month DESC, ap.advance_id DESC
</select>
<!-- balance = principal_amount (초기 등록 시 잔액=원금) -->
<insert id="insert" useGeneratedKeys="true" keyProperty="advanceId">
INSERT INTO advance_payment
(agent_id, advance_month, principal_amount, recovered_amount, balance,
status, memo, created_by, created_at)
VALUES
(#{agentId}, #{advanceMonth}, #{principalAmount}, 0, #{principalAmount},
'OUTSTANDING', #{memo}, #{createdBy}, NOW())
</insert>
<!-- 회수 후 갱신 — recovered_amount, balance, status, updated_at -->
<update id="update">
UPDATE advance_payment
SET recovered_amount = #{recoveredAmount},
balance = #{balance},
status = #{status},
memo = #{memo},
updated_at = NOW()
WHERE advance_id = #{advanceId}
</update>
<delete id="delete">
DELETE FROM advance_payment WHERE advance_id = #{advanceId}
</delete>
<!-- 설계사의 미상환 잔액 합계 — OUTSTANDING/PARTIAL 대상 -->
<select id="outstandingByAgent" resultType="java.math.BigDecimal">
SELECT COALESCE(SUM(balance), 0)
FROM advance_payment
WHERE agent_id = #{agentId}
AND status IN ('OUTSTANDING', 'PARTIAL')
</select>
<!-- FIFO 상계 대상 목록 — 오래된 발생월 우선 -->
<select id="selectOutstandingFifo" resultMap="VOMap">
SELECT advance_id, agent_id, advance_month,
principal_amount, recovered_amount, balance,
status, memo, created_at, created_by, updated_at
FROM advance_payment
WHERE agent_id = #{agentId}
AND status IN ('OUTSTANDING', 'PARTIAL')
ORDER BY advance_month ASC, advance_id ASC
</select>
</mapper>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.commission.AdvanceRecoveryLedgerMapper">
<resultMap id="VOMap" type="com.ga.core.vo.commission.AdvanceRecoveryLedgerVO"/>
<insert id="insert" useGeneratedKeys="true" keyProperty="recoveryId">
INSERT INTO advance_recovery_ledger
(advance_id, settle_month, recovery_amount, created_by, created_at)
VALUES
(#{advanceId}, #{settleMonth}, #{recoveryAmount}, #{createdBy}, NOW())
</insert>
<!--
배치 멱등용: 당월 상계 이력 삭제.
P9 reverseClawbackRecovery 패턴 — 재실행 시 역산 후 재적용.
-->
<delete id="deleteBySettleMonth">
DELETE FROM advance_recovery_ledger
WHERE advance_id = #{advanceId}
AND settle_month = #{settleMonth}
</delete>
<select id="selectBySettleMonth" resultMap="VOMap">
SELECT recovery_id, advance_id, settle_month, recovery_amount,
created_at, created_by
FROM advance_recovery_ledger
WHERE advance_id = #{advanceId}
AND settle_month = #{settleMonth}
ORDER BY recovery_id ASC
</select>
<select id="selectAdvanceIdsBySettleMonth" resultType="long">
SELECT DISTINCT advance_id
FROM advance_recovery_ledger
WHERE settle_month = #{settleMonth}
</select>
</mapper>
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.commission.CarrierReconciliationInMapper">
<resultMap id="VOMap" type="com.ga.core.vo.commission.CarrierReconciliationInVO"/>
<resultMap id="RespMap" type="com.ga.core.vo.commission.CarrierReconciliationInResp"/>
<!-- 목록/단건 조회 컬럼 — 보험사명 조인 포함 -->
<sql id="criJoinCols">
cr.recon_id,
cr.company_id, ic.company_code, ic.company_name,
cr.settle_month,
cr.file_name,
cr.record_count,
cr.reported_total,
cr.system_total,
cr.variance_amount,
cr.status,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'RECON_IN_STATUS' AND cc.code = cr.status) AS status_name,
cr.created_at
</sql>
<sql id="criJoins">
FROM carrier_reconciliation_in cr
LEFT JOIN insurance_company ic ON ic.company_id = cr.company_id
</sql>
<select id="selectById" resultMap="RespMap">
SELECT <include refid="criJoinCols"/>
<include refid="criJoins"/>
WHERE cr.recon_id = #{reconId}
</select>
<select id="selectList" resultMap="RespMap">
SELECT <include refid="criJoinCols"/>
<include refid="criJoins"/>
<where>
<if test="companyId != null">AND cr.company_id = #{companyId}</if>
<if test="settleMonth != null and settleMonth != ''">AND cr.settle_month = #{settleMonth}</if>
<if test="status != null and status != ''">AND cr.status = #{status}</if>
</where>
ORDER BY cr.settle_month DESC, cr.recon_id DESC
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="reconId">
INSERT INTO carrier_reconciliation_in
(company_id, settle_month, file_name, record_count,
reported_total, system_total, variance_amount, status,
created_by, created_at)
VALUES
(#{companyId}, #{settleMonth}, #{fileName}, COALESCE(#{recordCount}, 0),
COALESCE(#{reportedTotal}, 0), COALESCE(#{systemTotal}, 0),
COALESCE(#{varianceAmount}, 0),
COALESCE(#{status}, 'RECEIVED'), #{createdBy}, NOW())
</insert>
<update id="update">
UPDATE carrier_reconciliation_in
SET file_name = #{fileName},
record_count = #{recordCount},
reported_total = #{reportedTotal},
system_total = #{systemTotal},
variance_amount = #{varianceAmount},
status = #{status},
updated_at = NOW()
WHERE recon_id = #{reconId}
</update>
<!--
보험사 + 정산월 기준 upsert.
UNIQUE(company_id, settle_month) 충돌 시 갱신 — 멱등 재처리 지원.
-->
<insert id="upsert" useGeneratedKeys="true" keyProperty="reconId">
INSERT INTO carrier_reconciliation_in
(company_id, settle_month, file_name, record_count,
reported_total, system_total, variance_amount, status,
created_by, created_at)
VALUES
(#{companyId}, #{settleMonth}, #{fileName}, COALESCE(#{recordCount}, 0),
COALESCE(#{reportedTotal}, 0), COALESCE(#{systemTotal}, 0),
COALESCE(#{varianceAmount}, 0),
COALESCE(#{status}, 'RECEIVED'), #{createdBy}, NOW())
ON CONFLICT (company_id, settle_month) DO UPDATE
SET file_name = EXCLUDED.file_name,
record_count = EXCLUDED.record_count,
reported_total = EXCLUDED.reported_total,
system_total = EXCLUDED.system_total,
variance_amount = EXCLUDED.variance_amount,
status = EXCLUDED.status,
updated_at = NOW()
</insert>
</mapper>
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.commission.ChargebackWaiverRequestMapper">
<resultMap id="VOMap" type="com.ga.core.vo.commission.ChargebackWaiverRequestVO"/>
<resultMap id="RespMap" type="com.ga.core.vo.commission.ChargebackWaiverRequestResp"/>
<!-- 목록/단건 조회 컬럼 — 설계사명·증권번호·공통코드명 조인 포함 -->
<sql id="cwrJoinCols">
wr.request_id,
wr.contract_id, ct.policy_no,
wr.agent_id, a.agent_name, o.org_name,
wr.settle_month,
wr.original_chargeback,
wr.waiver_amount,
wr.reason,
wr.approval_status,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'WAIVER_STATUS' AND cc.code = wr.approval_status) AS approval_status_name,
wr.approver_id,
wr.approved_at,
wr.created_at
</sql>
<sql id="cwrJoins">
FROM chargeback_waiver_request wr
LEFT JOIN agent a ON a.agent_id = wr.agent_id
LEFT JOIN organization o ON o.org_id = a.org_id
LEFT JOIN contract ct ON ct.contract_id = wr.contract_id
</sql>
<select id="selectById" resultMap="RespMap">
SELECT <include refid="cwrJoinCols"/>
<include refid="cwrJoins"/>
WHERE wr.request_id = #{requestId}
</select>
<select id="selectList" resultMap="RespMap">
SELECT <include refid="cwrJoinCols"/>
<include refid="cwrJoins"/>
<where>
<if test="agentId != null">AND wr.agent_id = #{agentId}</if>
<if test="agentName != null and agentName != ''">AND a.agent_name LIKE '%' || #{agentName} || '%'</if>
<if test="settleMonth != null and settleMonth != ''">AND wr.settle_month = #{settleMonth}</if>
<if test="approvalStatus != null and approvalStatus != ''">AND wr.approval_status = #{approvalStatus}</if>
</where>
ORDER BY wr.settle_month DESC, wr.request_id DESC
</select>
<!-- approval_status 기본 'REQUESTED' -->
<insert id="insert" useGeneratedKeys="true" keyProperty="requestId">
INSERT INTO chargeback_waiver_request
(contract_id, agent_id, settle_month, original_chargeback,
waiver_amount, reason, approval_status, created_by, created_at)
VALUES
(#{contractId}, #{agentId}, #{settleMonth}, #{originalChargeback},
#{waiverAmount}, #{reason}, 'REQUESTED', #{createdBy}, NOW())
</insert>
<!-- 승인/반려 처리 — approval_status, approver_id, approved_at, updated_at 갱신 -->
<update id="updateApproval">
UPDATE chargeback_waiver_request
SET approval_status = #{approvalStatus},
approver_id = #{approverId},
approved_at = #{approvedAt},
updated_at = NOW()
WHERE request_id = #{requestId}
</update>
</mapper>
@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.commission.ClawbackInstallmentPlanMapper">
<resultMap id="VOMap" type="com.ga.core.vo.commission.ClawbackInstallmentPlanVO"/>
<resultMap id="RespMap" type="com.ga.core.vo.commission.ClawbackInstallmentPlanResp"/>
<!-- 목록/단건 조회 컬럼 — 채권 정보(설계사명·발생월) 조인 포함 -->
<sql id="planJoinCols">
p.plan_id,
p.receivable_id,
r.origin_settle_month,
r.agent_id, a.agent_name,
p.installment_no,
p.installment_amount,
p.due_month,
p.status,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'CB_INSTALLMENT_STATUS' AND cc.code = p.status) AS status_name,
p.recovered_at
</sql>
<sql id="planJoins">
FROM clawback_installment_plan p
JOIN clawback_receivable r ON r.receivable_id = p.receivable_id
LEFT JOIN agent a ON a.agent_id = r.agent_id
</sql>
<select id="selectById" resultMap="RespMap">
SELECT <include refid="planJoinCols"/>
<include refid="planJoins"/>
WHERE p.plan_id = #{planId}
</select>
<select id="selectList" resultMap="RespMap">
SELECT <include refid="planJoinCols"/>
<include refid="planJoins"/>
<where>
<if test="receivableId != null">AND p.receivable_id = #{receivableId}</if>
<if test="agentId != null">AND r.agent_id = #{agentId}</if>
<if test="dueMonth != null and dueMonth != ''">AND p.due_month = #{dueMonth}</if>
<if test="status != null and status != ''">AND p.status = #{status}</if>
</where>
ORDER BY p.due_month ASC, p.installment_no ASC
</select>
<!-- 채권 ID 기준 VO 목록 — 배치/서비스 내부 용도 -->
<select id="selectByReceivable" resultMap="VOMap">
SELECT plan_id, receivable_id, installment_no, installment_amount,
due_month, status, recovered_at,
created_at, created_by, updated_at
FROM clawback_installment_plan
WHERE receivable_id = #{receivableId}
ORDER BY installment_no ASC
</select>
<!--
분할상환 계획 일괄 등록.
MyBatis foreach — Service에서 List<ClawbackInstallmentPlanVO> 전달.
-->
<insert id="insertBatch" useGeneratedKeys="true" keyProperty="planId">
INSERT INTO clawback_installment_plan
(receivable_id, installment_no, installment_amount, due_month,
status, created_by, created_at)
VALUES
<foreach collection="list" item="p" separator=",">
(#{p.receivableId}, #{p.installmentNo}, #{p.installmentAmount}, #{p.dueMonth},
COALESCE(#{p.status}, 'SCHEDULED'), #{p.createdBy}, NOW())
</foreach>
</insert>
<!-- 회차 상태 갱신 — RECOVERED 시 recovered_at=NOW() -->
<update id="updateStatus">
UPDATE clawback_installment_plan
SET status = #{status},
recovered_at = CASE WHEN #{status} = 'RECOVERED' THEN NOW() ELSE recovered_at END,
updated_at = NOW()
WHERE plan_id = #{planId}
</update>
</mapper>
@@ -194,4 +194,30 @@
</where>
</select>
<!-- [MOD-6] 시효 만료 대상 채권 조회 — beforeMonth = baseMonth - expiration_months -->
<select id="selectExpired" resultMap="VOMap">
SELECT receivable_id, agent_id, origin_settle_month,
original_amount, recovered_amount, balance,
status, last_recovery_month,
writeoff_reason, written_off_at, written_off_by,
created_at, created_by, updated_at
FROM clawback_receivable
WHERE origin_settle_month &lt;= #{beforeMonth}
AND status IN ('OUTSTANDING', 'RECOVERING')
ORDER BY origin_settle_month ASC, receivable_id ASC
</select>
<!-- [MOD-6] 만료 대상 채권 일괄 대손 처리 — WRITTEN_OFF 상태 전이 -->
<update id="writeOffExpired">
UPDATE clawback_receivable
SET balance = 0,
status = 'WRITTEN_OFF',
writeoff_reason = #{reason},
written_off_at = NOW(),
updated_at = NOW()
WHERE origin_settle_month &lt;= #{beforeMonth}
AND status IN ('OUTSTANDING', 'RECOVERING')
</update>
</mapper>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.commission.LapsedReinstatementRecoveryMapper">
<resultMap id="VOMap" type="com.ga.core.vo.commission.LapsedReinstatementRecoveryVO"/>
<resultMap id="RespMap" type="com.ga.core.vo.commission.LapsedReinstatementRecoveryResp"/>
<!-- 목록/단건 조회 컬럼 — 설계사명·증권번호 조인 포함 -->
<sql id="lrrJoinCols">
lr.recovery_id,
lr.contract_id, ct.policy_no,
lr.agent_id, a.agent_name, o.org_name,
lr.lapsed_month,
lr.reinstated_month,
lr.withheld_amount,
lr.repaid_amount,
lr.status,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'LAPSE_RECOVERY_STATUS' AND cc.code = lr.status) AS status_name
</sql>
<sql id="lrrJoins">
FROM lapsed_reinstatement_recovery lr
LEFT JOIN agent a ON a.agent_id = lr.agent_id
LEFT JOIN organization o ON o.org_id = a.org_id
LEFT JOIN contract ct ON ct.contract_id = lr.contract_id
</sql>
<select id="selectById" resultMap="RespMap">
SELECT <include refid="lrrJoinCols"/>
<include refid="lrrJoins"/>
WHERE lr.recovery_id = #{recoveryId}
</select>
<select id="selectList" resultMap="RespMap">
SELECT <include refid="lrrJoinCols"/>
<include refid="lrrJoins"/>
<where>
<if test="agentId != null">AND lr.agent_id = #{agentId}</if>
<if test="agentName != null and agentName != ''">AND a.agent_name LIKE '%' || #{agentName} || '%'</if>
<if test="reinstatedMonth != null and reinstatedMonth != ''">AND lr.reinstated_month = #{reinstatedMonth}</if>
<if test="lapsedMonth != null and lapsedMonth != ''">AND lr.lapsed_month = #{lapsedMonth}</if>
<if test="status != null and status != ''">AND lr.status = #{status}</if>
</where>
ORDER BY lr.reinstated_month DESC, lr.recovery_id DESC
</select>
<!-- repaid_amount 기본 0, status 기본 PENDING -->
<insert id="insert" useGeneratedKeys="true" keyProperty="recoveryId">
INSERT INTO lapsed_reinstatement_recovery
(contract_id, agent_id, lapsed_month, reinstated_month,
withheld_amount, repaid_amount, status, created_by, created_at)
VALUES
(#{contractId}, #{agentId}, #{lapsedMonth}, #{reinstatedMonth},
#{withheldAmount}, 0, 'PENDING', #{createdBy}, NOW())
</insert>
<!-- 상태 + 재지급액 갱신 — REPAID 시 repaid_amount=withheld_amount(Service에서 전달) -->
<update id="updateStatus">
UPDATE lapsed_reinstatement_recovery
SET status = #{status},
repaid_amount = #{repaidAmount},
updated_at = NOW()
WHERE recovery_id = #{recoveryId}
</update>
<!-- 정산월 기준 설계사별 재지급 합계 — status='REPAID' AND reinstated_month=settleMonth -->
<select id="aggregateByAgent" resultType="map">
SELECT agent_id AS "agentId",
SUM(repaid_amount) AS "total"
FROM lapsed_reinstatement_recovery
WHERE reinstated_month = #{settleMonth}
AND status = 'REPAID'
GROUP BY agent_id
</select>
</mapper>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.commission.ReconciliationVarianceMapper">
<resultMap id="VOMap" type="com.ga.core.vo.commission.ReconciliationVarianceVO"/>
<!--
차이 상세 일괄 등록.
MyBatis foreach — Service에서 List<ReconciliationVarianceVO> 전달.
-->
<insert id="insertBatch">
INSERT INTO reconciliation_variance
(recon_id, policy_no, expected_amount, reported_amount,
variance_reason, created_by, created_at)
VALUES
<foreach collection="list" item="v" separator=",">
(#{v.reconId}, #{v.policyNo}, #{v.expectedAmount}, #{v.reportedAmount},
#{v.varianceReason}, #{v.createdBy}, NOW())
</foreach>
</insert>
<!-- 대사 입수 ID 기준 차이 상세 목록 조회 -->
<select id="selectByRecon" resultMap="VOMap">
SELECT variance_id, recon_id, policy_no,
expected_amount, reported_amount, variance_reason,
created_at, created_by
FROM reconciliation_variance
WHERE recon_id = #{reconId}
ORDER BY variance_id ASC
</select>
</mapper>
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.commission.RecruiterCommissionLedgerMapper">
<resultMap id="VOMap" type="com.ga.core.vo.commission.RecruiterCommissionLedgerVO"/>
<resultMap id="RespMap" type="com.ga.core.vo.commission.RecruiterCommissionLedgerResp"/>
<!-- 목록/단건 조회 컬럼 — 도입자·피도입 설계사명·공통코드명 조인 포함 -->
<sql id="ledgerJoinCols">
l.ledger_id,
l.recruiter_agent_id,
ra.agent_name AS recruiter_agent_name,
l.recruit_agent_id,
na.agent_name AS recruit_agent_name,
l.settle_month,
l.recruiter_type,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'RECRUITER_TYPE' AND cc.code = l.recruiter_type) AS recruiter_type_name,
l.base_amount,
l.commission_amount,
l.status,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'INCOME_STATUS' AND cc.code = l.status) AS status_name
</sql>
<sql id="ledgerJoins">
FROM recruiter_commission_ledger l
LEFT JOIN agent ra ON ra.agent_id = l.recruiter_agent_id
LEFT JOIN agent na ON na.agent_id = l.recruit_agent_id
</sql>
<select id="selectById" resultMap="RespMap">
SELECT <include refid="ledgerJoinCols"/>
<include refid="ledgerJoins"/>
WHERE l.ledger_id = #{ledgerId}
</select>
<select id="selectList" resultMap="RespMap">
SELECT <include refid="ledgerJoinCols"/>
<include refid="ledgerJoins"/>
<where>
<if test="recruiterAgentId != null">AND l.recruiter_agent_id = #{recruiterAgentId}</if>
<if test="recruitAgentId != null">AND l.recruit_agent_id = #{recruitAgentId}</if>
<if test="settleMonth != null and settleMonth != ''">AND l.settle_month = #{settleMonth}</if>
<if test="recruiterType != null and recruiterType != ''">AND l.recruiter_type = #{recruiterType}</if>
<if test="status != null and status != ''">AND l.status = #{status}</if>
</where>
ORDER BY l.settle_month DESC, l.recruiter_agent_id, l.ledger_id DESC
</select>
<!-- status 기본값 'CALCULATED' -->
<insert id="insert" useGeneratedKeys="true" keyProperty="ledgerId">
INSERT INTO recruiter_commission_ledger
(recruiter_agent_id, recruit_agent_id, settle_month, recruiter_type,
base_amount, commission_amount, status, created_by, created_at)
VALUES
(#{recruiterAgentId}, #{recruitAgentId}, #{settleMonth}, #{recruiterType},
#{baseAmount}, #{commissionAmount},
COALESCE(#{status}, 'CALCULATED'), #{createdBy}, NOW())
</insert>
<update id="updateStatus">
UPDATE recruiter_commission_ledger
SET status = #{status}, updated_at = NOW()
WHERE ledger_id = #{ledgerId}
</update>
<delete id="delete">
DELETE FROM recruiter_commission_ledger WHERE ledger_id = #{ledgerId}
</delete>
<!-- 정산월 기준 도입자 설계사별 도입수수료 합계 — CANCELLED 제외, agentId alias -->
<select id="aggregateByAgent" resultType="map">
SELECT recruiter_agent_id AS "agentId",
SUM(commission_amount) AS "total"
FROM recruiter_commission_ledger
WHERE settle_month = #{settleMonth}
AND status != 'CANCELLED'
GROUP BY recruiter_agent_id
</select>
</mapper>
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.commission.RecruiterCommissionRuleMapper">
<resultMap id="VOMap" type="com.ga.core.vo.commission.RecruiterCommissionRuleVO"/>
<resultMap id="RespMap" type="com.ga.core.vo.commission.RecruiterCommissionRuleResp"/>
<!-- 목록/단건 조회 컬럼 — 공통코드명 포함 -->
<sql id="ruleCols">
r.rule_id,
r.recruiter_type,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'RECRUITER_TYPE' AND cc.code = r.recruiter_type) AS recruiter_type_name,
r.calc_type,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code = 'CALC_TYPE' AND cc.code = r.calc_type) AS calc_type_name,
r.commission_rate,
r.fixed_amount,
r.effective_from,
r.effective_to
</sql>
<select id="selectById" resultMap="RespMap">
SELECT <include refid="ruleCols"/>
FROM recruiter_commission_rule r
WHERE r.rule_id = #{ruleId}
</select>
<select id="selectList" resultMap="RespMap">
SELECT <include refid="ruleCols"/>
FROM recruiter_commission_rule r
<where>
<if test="recruiterType != null and recruiterType != ''">AND r.recruiter_type = #{recruiterType}</if>
<if test="calcType != null and calcType != ''">AND r.calc_type = #{calcType}</if>
</where>
ORDER BY r.effective_from DESC, r.rule_id DESC
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="ruleId">
INSERT INTO recruiter_commission_rule
(recruiter_type, calc_type, commission_rate, fixed_amount,
effective_from, effective_to, created_by, created_at)
VALUES
(#{recruiterType}, #{calcType}, #{commissionRate}, #{fixedAmount},
#{effectiveFrom}, #{effectiveTo}, #{createdBy}, NOW())
</insert>
<update id="update">
UPDATE recruiter_commission_rule
SET recruiter_type = #{recruiterType},
calc_type = #{calcType},
commission_rate = #{commissionRate},
fixed_amount = #{fixedAmount},
effective_from = #{effectiveFrom},
effective_to = #{effectiveTo},
updated_at = NOW()
WHERE rule_id = #{ruleId}
</update>
<delete id="delete">
DELETE FROM recruiter_commission_rule WHERE rule_id = #{ruleId}
</delete>
</mapper>
@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ga.core.mapper.commission.StatuteConfigMapper">
<resultMap id="VOMap" type="com.ga.core.vo.commission.StatuteConfigVO"/>
<resultMap id="RespMap" type="com.ga.core.vo.commission.StatuteConfigResp"/>
<select id="selectById" resultMap="RespMap">
SELECT config_id, entity_type, expiration_months, is_active
FROM statute_config
WHERE config_id = #{configId}
</select>
<!-- entity_type UNIQUE — 단건 반환 -->
<select id="selectByEntityType" resultMap="RespMap">
SELECT config_id, entity_type, expiration_months, is_active
FROM statute_config
WHERE entity_type = #{entityType}
</select>
<select id="selectList" resultMap="RespMap">
SELECT config_id, entity_type, expiration_months, is_active
FROM statute_config
ORDER BY config_id ASC
</select>
<insert id="insert" useGeneratedKeys="true" keyProperty="configId">
INSERT INTO statute_config
(entity_type, expiration_months, is_active, created_by, created_at)
VALUES
(#{entityType}, #{expirationMonths}, COALESCE(#{isActive}, 'Y'),
#{createdBy}, NOW())
</insert>
<update id="update">
UPDATE statute_config
SET expiration_months = #{expirationMonths},
is_active = #{isActive},
updated_at = NOW()
WHERE config_id = #{configId}
</update>
</mapper>
@@ -18,7 +18,10 @@
ct.status,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code='CONTRACT_STATUS' AND cc.code=ct.status) AS status_name,
ct.contract_date, ct.effective_date, ct.lapse_date
ct.contract_date, ct.effective_date, ct.lapse_date,
ct.sales_channel,
(SELECT cc.code_name FROM common_code cc
WHERE cc.group_code='SALES_CHANNEL' AND cc.code=ct.sales_channel) AS sales_channel_name
</sql>
<select id="selectList" resultMap="ContractRespMap">
@@ -84,11 +87,11 @@
<insert id="insert" parameterType="com.ga.core.vo.product.ContractVO" useGeneratedKeys="true" keyProperty="contractId">
INSERT INTO contract (agent_id, product_id, policy_no, contractor_name, insured_name,
premium, pay_cycle, pay_period, coverage_period, status,
contract_date, effective_date, created_at)
contract_date, effective_date, sales_channel, created_at)
VALUES (#{agentId}, #{productId}, #{policyNo}, #{contractorName}, #{insuredName},
#{premium}, #{payCycle}, #{payPeriod}, #{coveragePeriod},
COALESCE(#{status},'ACTIVE'),
#{contractDate}, #{effectiveDate}, #{createdAt})
#{contractDate}, #{effectiveDate}, #{salesChannel}, #{createdAt})
</insert>
<update id="update" parameterType="com.ga.core.vo.product.ContractVO">
@@ -105,6 +108,7 @@
effective_date = #{effectiveDate},
lapse_date = #{lapseDate},
revival_date = #{revivalDate},
sales_channel = #{salesChannel},
updated_at = #{updatedAt}
WHERE contract_id = #{contractId}
</update>
@@ -24,7 +24,8 @@
WHERE product_id = #{productId} AND commission_year = #{year}
AND effective_from &lt;= #{baseDate}
AND (effective_to IS NULL OR effective_to &gt;= #{baseDate})
ORDER BY effective_from DESC, version DESC
AND (sales_channel = #{salesChannel} OR sales_channel IS NULL)
ORDER BY (sales_channel IS NULL) ASC, effective_from DESC, version DESC
LIMIT 1
</select>
@@ -65,7 +66,8 @@
AND commission_year = #{year}
AND effective_from &lt;= #{baseDate}
AND (effective_to IS NULL OR effective_to &gt;= #{baseDate})
ORDER BY effective_from DESC, version DESC
AND (sales_channel = #{salesChannel} OR sales_channel IS NULL)
ORDER BY (sales_channel IS NULL) ASC, effective_from DESC, version DESC
LIMIT 1
</select>