동기화
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
package com.ga.batch.settlement.domain;
|
||||
|
||||
import com.ga.core.mapper.chargeback.ChargebackGradeMapper;
|
||||
import com.ga.core.vo.chargeback.ChargebackGradeVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
/**
|
||||
* 배치용 차등환수 계산기. ga-api ChargebackCalculator와 동일 로직.
|
||||
* chargeback_grade 테이블 기반 5단계 환수율 조회.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class BatchChargebackCalculator {
|
||||
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
private final ChargebackGradeMapper gradeMapper;
|
||||
|
||||
public ChargebackResult calculate(LocalDate contractDate, LocalDate terminationDate,
|
||||
BigDecimal originalCommission, String productCategory) {
|
||||
int monthsElapsed = (int) ChronoUnit.MONTHS.between(contractDate, terminationDate);
|
||||
String today = LocalDate.now().format(DATE_FMT);
|
||||
|
||||
ChargebackGradeVO grade = gradeMapper.selectByMonths(productCategory, monthsElapsed, today);
|
||||
|
||||
if (grade == null) {
|
||||
return new ChargebackResult(BigDecimal.ZERO, monthsElapsed, BigDecimal.ZERO,
|
||||
"적용 환수 구간 없음");
|
||||
}
|
||||
|
||||
BigDecimal chargebackAmount = originalCommission
|
||||
.multiply(grade.getChargebackPercent())
|
||||
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP);
|
||||
|
||||
return new ChargebackResult(
|
||||
chargebackAmount,
|
||||
monthsElapsed,
|
||||
grade.getChargebackPercent(),
|
||||
String.format("경과월 %d개월 → 환수율 %s%%", monthsElapsed, grade.getChargebackPercent())
|
||||
);
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package com.ga.batch.settlement.domain;
|
||||
|
||||
import com.ga.core.mapper.installment.InstallmentRatioMapper;
|
||||
import com.ga.core.vo.installment.InstallmentPlanVO;
|
||||
import com.ga.core.vo.installment.InstallmentRatioVO;
|
||||
import com.ga.core.vo.installment.InstallmentStatus;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 배치용 분급 계획 생성기. ga-api InstallmentPlanGenerator와 동일 로직.
|
||||
* 계약 체결일 기준 1~7차년 분급 계획 생성.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class BatchInstallmentPlanGenerator {
|
||||
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyyMM");
|
||||
|
||||
private final InstallmentRatioMapper ratioMapper;
|
||||
|
||||
public List<InstallmentPlanVO> generate(Long contractId, BigDecimal totalCommission,
|
||||
LocalDate contractDate, String productCategory) {
|
||||
String baseDate = contractDate.format(DATE_FMT);
|
||||
List<InstallmentRatioVO> ratios = ratioMapper.selectAllActiveRatios(productCategory, baseDate);
|
||||
|
||||
List<InstallmentPlanVO> plans = new ArrayList<>();
|
||||
for (InstallmentRatioVO ratio : ratios) {
|
||||
InstallmentPlanVO plan = new InstallmentPlanVO();
|
||||
plan.setContractId(contractId);
|
||||
plan.setPlanYear(ratio.getPlanYear());
|
||||
plan.setPlanMonth(1);
|
||||
plan.setSettleMonth(contractDate.plusYears(ratio.getPlanYear()).format(MONTH_FMT));
|
||||
plan.setExpectedAmount(
|
||||
totalCommission.multiply(ratio.getRatioPercent())
|
||||
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP)
|
||||
);
|
||||
plan.setStatus(InstallmentStatus.SCHEDULED.code());
|
||||
plans.add(plan);
|
||||
}
|
||||
return plans;
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.ga.batch.settlement.domain;
|
||||
|
||||
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
|
||||
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
|
||||
import com.ga.core.mapper.regulatory.RegulatoryLimitMapper;
|
||||
import com.ga.core.vo.regulatory.RegulatoryLimitVO;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 배치용 1200%룰 한도 체커. ga-api RegulatoryLimitChecker와 동일 로직.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class BatchRegulatoryLimitChecker {
|
||||
|
||||
private static final String TOTAL_RATIO = "TOTAL_RATIO";
|
||||
private static final String FIRST_YEAR_RATIO = "FIRST_YEAR_RATIO";
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
private final RegulatoryLimitMapper limitMapper;
|
||||
private final RecruitLedgerMapper recruitLedgerMapper;
|
||||
private final MaintainLedgerMapper maintainLedgerMapper;
|
||||
|
||||
/** 누적 수수료율 한도 체크 (1200%). 신계약 + 유지 원장 합산. */
|
||||
public ViolationResult checkContractTotal(Long contractId, BigDecimal additionalAmount, LocalDate baseDate) {
|
||||
String baseDateStr = baseDate.format(DATE_FMT);
|
||||
RegulatoryLimitVO limit = limitMapper.selectActiveByType(TOTAL_RATIO, null, baseDateStr);
|
||||
if (limit == null) {
|
||||
return ViolationResult.ok(additionalAmount, null, TOTAL_RATIO);
|
||||
}
|
||||
|
||||
BigDecimal currentTotal = sumPaidByContract(contractId);
|
||||
BigDecimal projected = currentTotal.add(additionalAmount);
|
||||
|
||||
if (projected.compareTo(limit.getLimitValue()) > 0) {
|
||||
return ViolationResult.violated(
|
||||
currentTotal,
|
||||
limit.getLimitValue(),
|
||||
TOTAL_RATIO,
|
||||
String.format("누적 수수료 한도 초과: 현재 %s, 추가 %s, 한도 %s %s",
|
||||
currentTotal, additionalAmount, limit.getLimitValue(), limit.getUnit())
|
||||
);
|
||||
}
|
||||
return ViolationResult.ok(currentTotal, limit.getLimitValue(), TOTAL_RATIO);
|
||||
}
|
||||
|
||||
/** 1차년 수수료율 한도 체크. */
|
||||
public ViolationResult checkFirstYear(Long contractId, BigDecimal additionalAmount, LocalDate baseDate) {
|
||||
String baseDateStr = baseDate.format(DATE_FMT);
|
||||
RegulatoryLimitVO limit = limitMapper.selectActiveByType(FIRST_YEAR_RATIO, null, baseDateStr);
|
||||
if (limit == null) {
|
||||
return ViolationResult.ok(additionalAmount, null, FIRST_YEAR_RATIO);
|
||||
}
|
||||
|
||||
BigDecimal currentTotal = nullSafe(recruitLedgerMapper.sumFirstYearByContract(contractId));
|
||||
BigDecimal projected = currentTotal.add(additionalAmount);
|
||||
|
||||
if (projected.compareTo(limit.getLimitValue()) > 0) {
|
||||
return ViolationResult.violated(
|
||||
currentTotal,
|
||||
limit.getLimitValue(),
|
||||
FIRST_YEAR_RATIO,
|
||||
String.format("1차년 수수료 한도 초과: 현재 %s, 추가 %s, 한도 %s %s",
|
||||
currentTotal, additionalAmount, limit.getLimitValue(), limit.getUnit())
|
||||
);
|
||||
}
|
||||
return ViolationResult.ok(currentTotal, limit.getLimitValue(), FIRST_YEAR_RATIO);
|
||||
}
|
||||
|
||||
private BigDecimal sumPaidByContract(Long contractId) {
|
||||
return nullSafe(recruitLedgerMapper.sumByContract(contractId))
|
||||
.add(nullSafe(maintainLedgerMapper.sumByContract(contractId)));
|
||||
}
|
||||
|
||||
private BigDecimal nullSafe(BigDecimal v) {
|
||||
return v != null ? v : BigDecimal.ZERO;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.ga.batch.settlement.domain;
|
||||
|
||||
import com.ga.common.system.SystemConfigService;
|
||||
import com.ga.core.vo.org.TaxType;
|
||||
import com.ga.core.vo.org.VatType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
|
||||
/**
|
||||
* 배치용 세금 계산기. ga-api TaxCalculator와 동일 로직, ga-batch 모듈 독립.
|
||||
* 사업소득 3.3% / 기타소득 8.8% + 지방소득세 10% + 부가세 10% (TAXABLE만).
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class BatchTaxCalculator {
|
||||
|
||||
private final SystemConfigService configService;
|
||||
|
||||
public TaxBreakdown calculate(BigDecimal amount, TaxType taxType, VatType vatType) {
|
||||
BigDecimal incomeRate = resolveIncomeRate(taxType);
|
||||
BigDecimal localRate = configService.getDecimal("TAX_RATE_LOCAL_INCOME", new BigDecimal("10.0"));
|
||||
BigDecimal vatRate = configService.getDecimal("TAX_RATE_VAT", new BigDecimal("10.0"));
|
||||
|
||||
BigDecimal incomeTax = amount.multiply(incomeRate)
|
||||
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP);
|
||||
BigDecimal localTax = incomeTax.multiply(localRate)
|
||||
.divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP);
|
||||
BigDecimal vat = VatType.TAXABLE.equals(vatType)
|
||||
? amount.multiply(vatRate).divide(BigDecimal.valueOf(100), 0, RoundingMode.HALF_UP)
|
||||
: BigDecimal.ZERO;
|
||||
BigDecimal netAmount = amount.subtract(incomeTax).subtract(localTax).add(vat);
|
||||
|
||||
return new TaxBreakdown(incomeTax, localTax, vat, netAmount);
|
||||
}
|
||||
|
||||
private BigDecimal resolveIncomeRate(TaxType taxType) {
|
||||
if (TaxType.OTHER_INCOME.equals(taxType)) {
|
||||
return configService.getDecimal("TAX_RATE_OTHER_INCOME", new BigDecimal("8.8"));
|
||||
}
|
||||
return configService.getDecimal("TAX_RATE_BUSINESS_INCOME", new BigDecimal("3.3"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.ga.batch.settlement.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 차등환수 계산 결과. */
|
||||
public record ChargebackResult(
|
||||
BigDecimal chargebackAmount,
|
||||
int monthsElapsed,
|
||||
BigDecimal appliedPercent,
|
||||
String reason
|
||||
) {}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.ga.batch.settlement.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 세금 계산 결과. ga-api TaxBreakdown과 동일 구조 (모듈 독립). */
|
||||
public record TaxBreakdown(
|
||||
BigDecimal incomeTax,
|
||||
BigDecimal localTax,
|
||||
BigDecimal vat,
|
||||
BigDecimal netAmount
|
||||
) {}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.ga.batch.settlement.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 1200%룰 검증 결과. */
|
||||
public record ViolationResult(
|
||||
boolean violated,
|
||||
BigDecimal currentTotal,
|
||||
BigDecimal limitAmount,
|
||||
String limitType,
|
||||
String message
|
||||
) {
|
||||
public static ViolationResult ok(BigDecimal currentTotal, BigDecimal limitAmount, String limitType) {
|
||||
return new ViolationResult(false, currentTotal, limitAmount, limitType, null);
|
||||
}
|
||||
|
||||
public static ViolationResult violated(BigDecimal currentTotal, BigDecimal limitAmount,
|
||||
String limitType, String message) {
|
||||
return new ViolationResult(true, currentTotal, limitAmount, limitType, message);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,28 @@
|
||||
package com.ga.batch.settlement.step;
|
||||
|
||||
import com.ga.batch.settlement.SettlementContext;
|
||||
import com.ga.common.system.SystemConfigService;
|
||||
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.ledger.ExceptionLedgerMapper;
|
||||
import com.ga.core.mapper.ledger.MaintainLedgerMapper;
|
||||
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
|
||||
import com.ga.core.mapper.org.AgentMapper;
|
||||
import com.ga.core.mapper.settle.AgentDeductionMapper;
|
||||
import com.ga.core.mapper.settle.ChargebackMapper;
|
||||
import com.ga.core.mapper.settle.CommissionStatementMapper;
|
||||
import com.ga.core.mapper.settle.OverrideSettleMapper;
|
||||
import com.ga.core.mapper.settle.SettleMasterMapper;
|
||||
import com.ga.core.vo.org.AgentResp;
|
||||
import com.ga.core.vo.org.AgentSearchParam;
|
||||
import com.ga.core.vo.org.AgentVO;
|
||||
import com.ga.core.vo.org.TaxType;
|
||||
import com.ga.core.vo.org.VatType;
|
||||
import com.ga.core.vo.settle.AgentDeductionVO;
|
||||
import com.ga.core.vo.settle.CommissionStatementVO;
|
||||
import com.ga.core.vo.settle.DeductionStatus;
|
||||
import com.ga.core.vo.settle.SettleMasterVO;
|
||||
import com.ga.core.vo.settle.StatementType;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.batch.core.StepContribution;
|
||||
@@ -23,12 +33,22 @@ import org.springframework.batch.repeat.RepeatStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Step 8 — aggregate: 설계사별 통합 집계 → settle_master UPSERT (세금 자동 계산)
|
||||
* Step 8 — aggregate: 설계사별 통합 집계 → settle_master UPSERT
|
||||
*
|
||||
* 통합 내용:
|
||||
* - BatchTaxCalculator: 설계사 세금유형(TaxType/VatType)에 따라 정확한 세금 계산
|
||||
* (사업소득 3.3% / 기타소득 8.8% + 지방소득세 10% + 부가세 10%)
|
||||
* - AgentDeductionMapper: PENDING 차감 합산 → netAmount에서 차감 후 APPLIED 상태로 전이
|
||||
* - CommissionStatementMapper: 설계사별 명세서 레코드(ISSUED 상태) INSERT
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -44,16 +64,19 @@ public class AggregateStep implements Tasklet {
|
||||
private final OverrideSettleMapper overrideMapper;
|
||||
private final ChargebackMapper chargebackMapper;
|
||||
private final SettleMasterMapper settleMapper;
|
||||
private final SystemConfigService configService;
|
||||
private final AgentDeductionMapper deductionMapper;
|
||||
private final CommissionStatementMapper statementMapper;
|
||||
private final BatchTaxCalculator taxCalculator;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
||||
log.info("[Step8 aggregate] settleMonth={}", ctx.getSettleMonth());
|
||||
|
||||
Map<Long, BigDecimal> recruitMap = toMap(recruitMapper.aggregateByAgent(ctx.getSettleMonth()));
|
||||
Map<Long, BigDecimal> maintainMap = toMap(maintainMapper.aggregateByAgent(ctx.getSettleMonth()));
|
||||
Map<Long, BigDecimal> chargebackMap= toMap(chargebackMapper.aggregateByAgent(ctx.getSettleMonth()));
|
||||
Map<Long, BigDecimal> overrideMap = toMap(overrideMapper.aggregateByUpper(ctx.getSettleMonth()));
|
||||
Map<Long, BigDecimal> recruitMap = toMap(recruitMapper.aggregateByAgent(ctx.getSettleMonth()));
|
||||
Map<Long, BigDecimal> maintainMap = toMap(maintainMapper.aggregateByAgent(ctx.getSettleMonth()));
|
||||
Map<Long, BigDecimal> chargebackMap = toMap(chargebackMapper.aggregateByAgent(ctx.getSettleMonth()));
|
||||
Map<Long, BigDecimal> overrideMap = toMap(overrideMapper.aggregateByUpper(ctx.getSettleMonth()));
|
||||
|
||||
// 예외는 PLUS/MINUS 분리
|
||||
Map<Long, BigDecimal> excPlus = new HashMap<>();
|
||||
Map<Long, BigDecimal> excMinus = new HashMap<>();
|
||||
@@ -63,10 +86,8 @@ public class AggregateStep implements Tasklet {
|
||||
excMinus.put(agentId, toBd(row.get("minus")));
|
||||
}
|
||||
|
||||
BigDecimal taxRate = configService.getDecimal("TAX_RATE", new BigDecimal("3.3"));
|
||||
|
||||
// 합집합 agent_id
|
||||
java.util.Set<Long> agentIds = new java.util.HashSet<>();
|
||||
Set<Long> agentIds = new HashSet<>();
|
||||
agentIds.addAll(recruitMap.keySet());
|
||||
agentIds.addAll(maintainMap.keySet());
|
||||
agentIds.addAll(chargebackMap.keySet());
|
||||
@@ -76,10 +97,23 @@ public class AggregateStep implements Tasklet {
|
||||
|
||||
// 활성 설계사 누락 보충
|
||||
AgentSearchParam ap = new AgentSearchParam();
|
||||
ap.setStatus("ACTIVE"); ap.setPageSize(Integer.MAX_VALUE);
|
||||
for (AgentResp a : agentMapper.selectList(ap)) agentIds.add(a.getAgentId());
|
||||
ap.setStatus("ACTIVE");
|
||||
ap.setPageSize(Integer.MAX_VALUE);
|
||||
for (AgentResp a : agentMapper.selectList(ap)) {
|
||||
agentIds.add(a.getAgentId());
|
||||
}
|
||||
|
||||
// 정산월 PENDING 차감 목록 일괄 조회 (배치 효율)
|
||||
List<AgentDeductionVO> allDeductions = deductionMapper.selectPendingByMonth(ctx.getSettleMonth());
|
||||
Map<Long, List<AgentDeductionVO>> deductionByAgent = new HashMap<>();
|
||||
for (AgentDeductionVO d : allDeductions) {
|
||||
deductionByAgent.computeIfAbsent(d.getAgentId(), k -> new ArrayList<>()).add(d);
|
||||
}
|
||||
|
||||
List<Long> appliedDeductionIds = new ArrayList<>();
|
||||
List<CommissionStatementVO> statements = new ArrayList<>();
|
||||
int count = 0;
|
||||
|
||||
for (Long agentId : agentIds) {
|
||||
BigDecimal recruit = recruitMap.getOrDefault(agentId, BigDecimal.ZERO);
|
||||
BigDecimal maintain = maintainMap.getOrDefault(agentId, BigDecimal.ZERO);
|
||||
@@ -90,9 +124,25 @@ public class AggregateStep implements Tasklet {
|
||||
|
||||
BigDecimal gross = MoneyUtil.add(MoneyUtil.add(MoneyUtil.add(recruit, maintain), ov), ep)
|
||||
.subtract(cb).subtract(em);
|
||||
BigDecimal tax = MoneyUtil.tax(gross, taxRate);
|
||||
BigDecimal net = MoneyUtil.sub(gross, tax);
|
||||
|
||||
// 세금유형 조회 (없으면 기본: BUSINESS_INCOME / NONE)
|
||||
AgentVO agentVO = agentMapper.selectById(agentId);
|
||||
TaxType taxType = parseTaxType(agentVO);
|
||||
VatType vatType = parseVatType(agentVO);
|
||||
|
||||
// BatchTaxCalculator로 정확한 세금 계산
|
||||
TaxBreakdown tax = taxCalculator.calculate(gross, taxType, vatType);
|
||||
BigDecimal totalTax = tax.incomeTax().add(tax.localTax()); // settle_master.tax_amount
|
||||
|
||||
// 차감 합산
|
||||
List<AgentDeductionVO> agentDeductions = deductionByAgent.getOrDefault(agentId, List.of());
|
||||
BigDecimal deductionTotal = agentDeductions.stream()
|
||||
.map(AgentDeductionVO::getAmount)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
|
||||
BigDecimal netAmount = tax.netAmount().subtract(deductionTotal);
|
||||
|
||||
// settle_master UPSERT
|
||||
SettleMasterVO vo = new SettleMasterVO();
|
||||
vo.setAgentId(agentId);
|
||||
vo.setSettleMonth(ctx.getSettleMonth());
|
||||
@@ -103,17 +153,74 @@ public class AggregateStep implements Tasklet {
|
||||
vo.setOverrideTotal(ov);
|
||||
vo.setChargebackTotal(cb);
|
||||
vo.setGrossAmount(gross);
|
||||
vo.setTaxAmount(tax);
|
||||
vo.setNetAmount(net);
|
||||
vo.setTaxAmount(totalTax); // incomeTax + localTax 합산
|
||||
vo.setNetAmount(netAmount); // gross - incomeTax - localTax + vat - deduction
|
||||
vo.setStatus("CALCULATED");
|
||||
settleMapper.upsert(vo);
|
||||
count++;
|
||||
|
||||
// 차감 적용 완료 처리
|
||||
if (!agentDeductions.isEmpty()) {
|
||||
agentDeductions.forEach(d -> appliedDeductionIds.add(d.getDeductionId()));
|
||||
}
|
||||
|
||||
// 명세서 레코드 생성 (commission_statement INSERT, 중복 시 skip)
|
||||
CommissionStatementVO existingStmt = statementMapper.selectByAgentMonth(
|
||||
agentId, ctx.getSettleMonth(), StatementType.MONTHLY.code());
|
||||
if (existingStmt == null && gross.signum() != 0) {
|
||||
CommissionStatementVO stmt = new CommissionStatementVO();
|
||||
stmt.setAgentId(agentId);
|
||||
stmt.setSettleMonth(ctx.getSettleMonth());
|
||||
stmt.setStatementType(StatementType.MONTHLY.code());
|
||||
stmt.setGrossAmount(gross);
|
||||
stmt.setIncomeTaxAmount(tax.incomeTax());
|
||||
stmt.setLocalTaxAmount(tax.localTax());
|
||||
stmt.setVatAmount(tax.vat());
|
||||
stmt.setDeductionAmount(deductionTotal);
|
||||
stmt.setNetAmount(netAmount);
|
||||
stmt.setIssueStatus("ISSUED");
|
||||
stmt.setIssuedAt(LocalDateTime.now());
|
||||
statements.add(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
// 차감 APPLIED 상태 일괄 전이
|
||||
if (!appliedDeductionIds.isEmpty()) {
|
||||
deductionMapper.updateStatusBatch(appliedDeductionIds, DeductionStatus.APPLIED.name());
|
||||
log.info("[Step8] deductions applied: {}", appliedDeductionIds.size());
|
||||
}
|
||||
|
||||
// 명세서 일괄 INSERT
|
||||
for (CommissionStatementVO stmt : statements) {
|
||||
statementMapper.insert(stmt);
|
||||
}
|
||||
if (!statements.isEmpty()) {
|
||||
log.info("[Step8] commission statements inserted: {}", statements.size());
|
||||
}
|
||||
|
||||
ctx.setAggregateCount(count);
|
||||
log.info("[Step8] settle master upserted: {}", count);
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
|
||||
private TaxType parseTaxType(AgentVO agent) {
|
||||
if (agent == null || agent.getTaxType() == null) return TaxType.BUSINESS_INCOME;
|
||||
try {
|
||||
return TaxType.valueOf(agent.getTaxType());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return TaxType.BUSINESS_INCOME;
|
||||
}
|
||||
}
|
||||
|
||||
private VatType parseVatType(AgentVO agent) {
|
||||
if (agent == null || agent.getVatType() == null) return VatType.NONE;
|
||||
try {
|
||||
return VatType.valueOf(agent.getVatType());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return VatType.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<Long, BigDecimal> toMap(List<Map<String, Object>> rows) {
|
||||
Map<Long, BigDecimal> m = new HashMap<>();
|
||||
for (Map<String, Object> r : rows) {
|
||||
|
||||
@@ -2,10 +2,15 @@ package com.ga.batch.settlement.step;
|
||||
|
||||
import com.ga.batch.settlement.SettlementContext;
|
||||
import com.ga.batch.settlement.calc.CommissionCalculator;
|
||||
import com.ga.batch.settlement.domain.BatchInstallmentPlanGenerator;
|
||||
import com.ga.batch.settlement.domain.BatchRegulatoryLimitChecker;
|
||||
import com.ga.batch.settlement.domain.ViolationResult;
|
||||
import com.ga.common.util.DateUtil;
|
||||
import com.ga.common.util.MoneyUtil;
|
||||
import com.ga.core.mapper.installment.InstallmentPlanMapper;
|
||||
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
|
||||
import com.ga.core.mapper.product.ContractMapper;
|
||||
import com.ga.core.vo.installment.InstallmentPlanVO;
|
||||
import com.ga.core.vo.ledger.LedgerVO;
|
||||
import com.ga.core.vo.product.ContractResp;
|
||||
import com.ga.core.vo.product.ContractSearchParam;
|
||||
@@ -25,6 +30,9 @@ import java.util.List;
|
||||
* Step 4 — calcRecruit: 모집 수수료 계산 (계약 첫 회차)
|
||||
*
|
||||
* 원칙: 계약시점의 commission_rate / payout_rule 적용 (effective_from 기준)
|
||||
*
|
||||
* 추가: 1200%룰(BatchRegulatoryLimitChecker) 검증 후 한도 초과 계약은
|
||||
* InstallmentPlan(분급 7년) 생성. Step 4.5 역할을 Step 4 내에서 수행.
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -36,6 +44,9 @@ public class CalcRecruitStep implements Tasklet {
|
||||
private final ContractMapper contractMapper;
|
||||
private final RecruitLedgerMapper recruitMapper;
|
||||
private final CommissionCalculator calculator;
|
||||
private final BatchRegulatoryLimitChecker regulatoryChecker;
|
||||
private final BatchInstallmentPlanGenerator installmentGenerator;
|
||||
private final InstallmentPlanMapper installmentPlanMapper;
|
||||
|
||||
private static final int BATCH_SIZE = 1000;
|
||||
|
||||
@@ -55,10 +66,33 @@ public class CalcRecruitStep implements Tasklet {
|
||||
recruitMapper.deleteBySettleMonth(ctx.getSettleMonth());
|
||||
|
||||
List<LedgerVO> batch = new ArrayList<>(BATCH_SIZE);
|
||||
List<InstallmentPlanVO> installmentBatch = new ArrayList<>();
|
||||
int total = 0;
|
||||
int violationCount = 0;
|
||||
|
||||
for (ContractResp c : contracts) {
|
||||
LedgerVO led = calculator.calcRecruitLedger(c, ctx.getSettleMonth());
|
||||
if (led == null || MoneyUtil.isZero(led.getAgentAmount())) continue;
|
||||
|
||||
// 1200%룰 검증 (1차년도 한도)
|
||||
if (c.getContractDate() != null) {
|
||||
ViolationResult firstYearCheck = regulatoryChecker.checkFirstYear(
|
||||
c.getContractId(), led.getAgentAmount(), c.getContractDate());
|
||||
|
||||
if (firstYearCheck.violated()) {
|
||||
log.warn("[Step4] 1200%룰 위반: contractId={}, msg={}",
|
||||
c.getContractId(), firstYearCheck.message());
|
||||
violationCount++;
|
||||
|
||||
// 1200%룰 위반 계약: 분급 계획 생성 (7년 35/25/15/10/7/5/3)
|
||||
List<InstallmentPlanVO> plans = installmentGenerator.generate(
|
||||
c.getContractId(), led.getAgentAmount(),
|
||||
c.getContractDate(), c.getInsuranceType());
|
||||
installmentBatch.addAll(plans);
|
||||
// 위반 계약도 원장에는 기록 (감사 목적)
|
||||
}
|
||||
}
|
||||
|
||||
batch.add(led);
|
||||
if (batch.size() >= BATCH_SIZE) {
|
||||
recruitMapper.batchInsert(batch);
|
||||
@@ -71,8 +105,15 @@ public class CalcRecruitStep implements Tasklet {
|
||||
total += batch.size();
|
||||
}
|
||||
|
||||
// 분급 계획 일괄 저장
|
||||
if (!installmentBatch.isEmpty()) {
|
||||
installmentPlanMapper.insertBatch(installmentBatch);
|
||||
log.info("[Step4] installment plans created: {} (violations: {})",
|
||||
installmentBatch.size(), violationCount);
|
||||
}
|
||||
|
||||
ctx.setRecruitCount(total);
|
||||
log.info("[Step4] recruit ledger inserted: {}", total);
|
||||
log.info("[Step4] recruit ledger inserted: {}, regulatory violations: {}", total, violationCount);
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.ga.batch.settlement.step;
|
||||
|
||||
import com.ga.batch.settlement.SettlementContext;
|
||||
import com.ga.batch.settlement.domain.BatchChargebackCalculator;
|
||||
import com.ga.batch.settlement.domain.ChargebackResult;
|
||||
import com.ga.common.util.DateUtil;
|
||||
import com.ga.common.util.MoneyUtil;
|
||||
import com.ga.core.mapper.ledger.RecruitLedgerMapper;
|
||||
import com.ga.core.mapper.product.ContractMapper;
|
||||
import com.ga.core.mapper.rule.RuleMapper;
|
||||
import com.ga.core.mapper.settle.ChargebackMapper;
|
||||
import com.ga.core.vo.product.ContractResp;
|
||||
import com.ga.core.vo.product.ContractSearchParam;
|
||||
@@ -26,7 +26,8 @@ import java.util.List;
|
||||
/**
|
||||
* Step 6 — chargebackException: 환수 자동생성 + 예외금액 자동계산
|
||||
*
|
||||
* 환수 대상: 정산월에 LAPSE(실효) 된 계약 — 계약일~실효까지 경과월수로 환수율 결정.
|
||||
* 환수율 계산: BatchChargebackCalculator (chargeback_grade 5단계 차등환수) 사용.
|
||||
* 환수 원금: 해당 계약의 모집 원장 합산 (recruit_ledger 기준).
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@@ -37,8 +38,8 @@ public class ChargebackExceptionStep implements Tasklet {
|
||||
private final SettlementContext ctx;
|
||||
private final ContractMapper contractMapper;
|
||||
private final RecruitLedgerMapper recruitMapper;
|
||||
private final RuleMapper ruleMapper;
|
||||
private final ChargebackMapper chargebackMapper;
|
||||
private final BatchChargebackCalculator chargebackCalculator;
|
||||
|
||||
@Override
|
||||
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) {
|
||||
@@ -51,30 +52,30 @@ public class ChargebackExceptionStep implements Tasklet {
|
||||
|
||||
List<ChargebackVO> chargebacks = new ArrayList<>();
|
||||
for (ContractResp c : lapsed) {
|
||||
if (c.getLapseDate() == null) continue;
|
||||
if (c.getLapseDate() == null || c.getContractDate() == null) continue;
|
||||
|
||||
String lapseMonth = DateUtil.getSettleMonth(c.getLapseDate());
|
||||
if (!ctx.getSettleMonth().equals(lapseMonth)) continue;
|
||||
if (c.getContractDate() == null) continue;
|
||||
|
||||
int monthsActive = DateUtil.calcMonthDiff(
|
||||
DateUtil.getSettleMonth(c.getContractDate()), lapseMonth);
|
||||
BigDecimal cbRate = ruleMapper.findChargebackRate(
|
||||
c.getCompanyCode(), c.getInsuranceType(), monthsActive, c.getLapseDate());
|
||||
if (cbRate == null || cbRate.signum() <= 0) continue;
|
||||
// 모집 원장에서 원 수수료 조회
|
||||
BigDecimal originalCommission = recruitMapper.sumByContract(c.getContractId());
|
||||
if (originalCommission == null || originalCommission.signum() <= 0) continue;
|
||||
|
||||
// 모집수수료 합계 × 환수율
|
||||
BigDecimal originalAmount = MoneyUtil.zero(c.getPremium())
|
||||
.multiply(cbRate)
|
||||
.divide(MoneyUtil.HUNDRED, 2, java.math.RoundingMode.HALF_UP);
|
||||
// BatchChargebackCalculator: chargeback_grade 5단계 차등환수
|
||||
ChargebackResult result = chargebackCalculator.calculate(
|
||||
c.getContractDate(), c.getLapseDate(),
|
||||
originalCommission, c.getInsuranceType());
|
||||
|
||||
if (result.chargebackAmount().signum() <= 0) continue;
|
||||
|
||||
ChargebackVO cb = new ChargebackVO();
|
||||
cb.setContractId(c.getContractId());
|
||||
cb.setAgentId(c.getAgentId());
|
||||
cb.setPolicyNo(c.getPolicyNo());
|
||||
cb.setLapseMonth(monthsActive);
|
||||
cb.setCbRate(cbRate);
|
||||
cb.setCbAmount(originalAmount);
|
||||
cb.setRemainAmount(originalAmount);
|
||||
cb.setLapseMonth(result.monthsElapsed());
|
||||
cb.setCbRate(result.appliedPercent());
|
||||
cb.setCbAmount(result.chargebackAmount());
|
||||
cb.setRemainAmount(result.chargebackAmount());
|
||||
cb.setSettleMonth(ctx.getSettleMonth());
|
||||
cb.setStatus("NEW");
|
||||
chargebacks.add(cb);
|
||||
@@ -84,7 +85,7 @@ public class ChargebackExceptionStep implements Tasklet {
|
||||
chargebackMapper.insertBatch(chargebacks);
|
||||
}
|
||||
ctx.setChargebackCount(chargebacks.size());
|
||||
log.info("[Step6] chargeback inserted: {}", chargebacks.size());
|
||||
log.info("[Step6] chargeback inserted: {} (using BatchChargebackCalculator)", chargebacks.size());
|
||||
return RepeatStatus.FINISHED;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user