package com.mydreamjob.salarycalculator; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.common.exception.ErrorCode; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.List; @Component public class SalaryCalculationEngine { public static final String TAX_RULES_VERSION = "CN_IIT_2019_V1"; private static final BigDecimal ZERO = new BigDecimal("0"); private static final BigDecimal MONTHLY_BASIC_DEDUCTION = new BigDecimal("5000"); private static final BigDecimal MAX_INPUT_AMOUNT = new BigDecimal("10000000"); private static final BigDecimal MAX_ILLNESS_FEE = new BigDecimal("10000"); private static final BigDecimal MAX_PENSION_RATE = new BigDecimal("0.20"); private static final BigDecimal MAX_MEDICAL_RATE = new BigDecimal("0.20"); private static final BigDecimal MAX_UNEMPLOYMENT_RATE = new BigDecimal("0.10"); private static final BigDecimal MAX_HOUSING_FUND_RATE = new BigDecimal("0.12"); private static final int MONEY_SCALE = 2; private static final List ANNUAL_BRACKETS = List.of( new TaxBracket(new BigDecimal("36000"), new BigDecimal("0.03"), new BigDecimal("0")), new TaxBracket(new BigDecimal("144000"), new BigDecimal("0.10"), new BigDecimal("2520")), new TaxBracket(new BigDecimal("300000"), new BigDecimal("0.20"), new BigDecimal("16920")), new TaxBracket(new BigDecimal("420000"), new BigDecimal("0.25"), new BigDecimal("31920")), new TaxBracket(new BigDecimal("660000"), new BigDecimal("0.30"), new BigDecimal("52920")), new TaxBracket(new BigDecimal("960000"), new BigDecimal("0.35"), new BigDecimal("85920")), new TaxBracket(null, new BigDecimal("0.45"), new BigDecimal("181920"))); public SalaryCalculationResult calculate(SalaryCalculationInput input, SalaryCityPolicy policy) { if (input == null || policy == null) { throw invalidParameter("input and city policy are required"); } int monthIndex = input.getMonthIndex() == null ? 1 : input.getMonthIndex(); BigDecimal gross = requiredMoney(input.getGrossMonthlySalary(), "grossMonthlySalary"); BigDecimal specialDeduction = optionalMoney( input.getSpecialAdditionalDeduction(), "specialAdditionalDeduction", MAX_INPUT_AMOUNT); SalaryOverrideParameters overrides = input.getOverrides(); if (gross.compareTo(ZERO) <= 0 || gross.compareTo(MAX_INPUT_AMOUNT) > 0) { throw invalidParameter("grossMonthlySalary must be within (0, 10000000]"); } if (monthIndex < 1 || monthIndex > 12) { throw invalidParameter("monthIndex must be between 1 and 12"); } BigDecimal pensionRate = effectiveRate( overrides == null ? null : overrides.getPensionRate(), policy.getPensionRate(), MAX_PENSION_RATE, "pensionRate"); BigDecimal medicalRate = effectiveRate( overrides == null ? null : overrides.getMedicalRate(), policy.getMedicalRate(), MAX_MEDICAL_RATE, "medicalRate"); BigDecimal unemploymentRate = effectiveRate( overrides == null ? null : overrides.getUnemploymentRate(), policy.getUnemploymentRate(), MAX_UNEMPLOYMENT_RATE, "unemploymentRate"); BigDecimal housingFundRate = effectiveRate( overrides == null ? null : overrides.getHousingFundRate(), policy.getHousingFundRate(), MAX_HOUSING_FUND_RATE, "housingFundRate"); BigDecimal illnessFee = effectiveFee( overrides == null ? null : overrides.getSeriousIllnessMonthlyFee(), policy.getSeriousIllnessMonthlyFee()); BigDecimal socialSecurityBase = effectiveBase( overrides == null ? null : overrides.getSocialSecurityBase(), gross, policy.getSocialSecurityBaseMin(), policy.getSocialSecurityBaseMax(), "socialSecurityBase"); BigDecimal housingFundBase = effectiveBase( overrides == null ? null : overrides.getHousingFundBase(), gross, policy.getHousingFundBaseMin(), policy.getHousingFundBaseMax(), "housingFundBase"); if (socialSecurityBase.compareTo(ZERO) == 0 && (pensionRate.signum() != 0 || medicalRate.signum() != 0 || unemploymentRate.signum() != 0)) { throw invalidParameter("socialSecurityBase may be zero only when social insurance rates are zero"); } if (housingFundBase.compareTo(ZERO) == 0 && housingFundRate.signum() != 0) { throw invalidParameter("housingFundBase may be zero only when housingFundRate is zero"); } BigDecimal pension = socialSecurityBase.multiply(pensionRate); BigDecimal medical = socialSecurityBase.multiply(medicalRate); BigDecimal unemployment = socialSecurityBase.multiply(unemploymentRate); BigDecimal housingFund = housingFundBase.multiply(housingFundRate); BigDecimal monthlySocialInsurance = pension.add(medical).add(unemployment); BigDecimal cumulativeGross = gross.multiply(BigDecimal.valueOf(monthIndex)); BigDecimal cumulativeSocialInsurance = monthlySocialInsurance.multiply(BigDecimal.valueOf(monthIndex)); BigDecimal cumulativeHousingFund = housingFund.multiply(BigDecimal.valueOf(monthIndex)); BigDecimal cumulativeIllness = illnessFee.multiply(BigDecimal.valueOf(monthIndex)); BigDecimal cumulativeBasicDeduction = MONTHLY_BASIC_DEDUCTION.multiply(BigDecimal.valueOf(monthIndex)); BigDecimal cumulativeSpecialDeduction = specialDeduction.multiply(BigDecimal.valueOf(monthIndex)); BigDecimal cumulativeTaxable = nonNegative(cumulativeGross .subtract(cumulativeSocialInsurance) .subtract(cumulativeHousingFund) .subtract(cumulativeIllness) .subtract(cumulativeBasicDeduction) .subtract(cumulativeSpecialDeduction)); BigDecimal cumulativeTaxDue = taxFor(cumulativeTaxable); int priorMonths = monthIndex - 1; BigDecimal priorTaxable = nonNegative(gross.multiply(BigDecimal.valueOf(priorMonths)) .subtract(monthlySocialInsurance.multiply(BigDecimal.valueOf(priorMonths))) .subtract(housingFund.multiply(BigDecimal.valueOf(priorMonths))) .subtract(illnessFee.multiply(BigDecimal.valueOf(priorMonths))) .subtract(MONTHLY_BASIC_DEDUCTION.multiply(BigDecimal.valueOf(priorMonths))) .subtract(specialDeduction.multiply(BigDecimal.valueOf(priorMonths)))); BigDecimal priorTax = taxFor(priorTaxable); BigDecimal currentTax = nonNegative(cumulativeTaxDue.subtract(priorTax)); BigDecimal netSalary = nonNegative(gross .subtract(pension) .subtract(medical) .subtract(unemployment) .subtract(housingFund) .subtract(illnessFee) .subtract(currentTax)); return SalaryCalculationResult.builder() .cityCode(policy.getCityCode()) .cityName(policy.getCityName()) .dataLevel(policy.getDataLevel()) .policyVersion(policy.getPolicyVersion()) .policyEffectiveDate(policy.getPolicyEffectiveDate()) .taxRulesVersion(TAX_RULES_VERSION) .estimate(true) .monthIndex(monthIndex) .grossMonthlySalary(money(gross)) .specialAdditionalDeduction(money(specialDeduction)) .socialSecurityBase(money(socialSecurityBase)) .housingFundBase(money(housingFundBase)) .pensionRate(rate(pensionRate)) .medicalRate(rate(medicalRate)) .unemploymentRate(rate(unemploymentRate)) .housingFundRate(rate(housingFundRate)) .seriousIllnessMonthlyFee(money(illnessFee)) .pension(money(pension)) .medical(money(medical)) .unemployment(money(unemployment)) .housingFund(money(housingFund)) .cumulativeGrossIncome(money(cumulativeGross)) .cumulativeSocialInsurance(money(cumulativeSocialInsurance)) .cumulativeHousingFund(money(cumulativeHousingFund)) .cumulativeSeriousIllness(money(cumulativeIllness)) .cumulativeBasicDeduction(money(cumulativeBasicDeduction)) .cumulativeSpecialAdditionalDeduction(money(cumulativeSpecialDeduction)) .cumulativeTaxableIncome(money(cumulativeTaxable)) .cumulativeTaxDue(money(cumulativeTaxDue)) .previouslyWithheldTax(money(priorTax)) .currentMonthTax(money(currentTax)) .netSalary(money(netSalary)) .build(); } private BigDecimal effectiveBase( BigDecimal override, BigDecimal gross, BigDecimal min, BigDecimal max, String field) { validatePolicyRange(min, max, field); if (override != null) { BigDecimal value = requiredMoney(override, field); if (value.compareTo(ZERO) < 0) { throw invalidParameter(field + " must not be negative"); } if (value.compareTo(ZERO) > 0 && (value.compareTo(min) < 0 || value.compareTo(max) > 0)) { throw invalidParameter(field + " must be within city range or zero for an exempt contribution"); } return value; } return clamp(gross, min, max); } private BigDecimal effectiveRate( BigDecimal override, BigDecimal fallback, BigDecimal max, String field) { BigDecimal value = override == null ? fallback : requiredRate(override, field); if (value == null || value.compareTo(ZERO) < 0 || value.compareTo(max) > 0) { throw invalidParameter(field + " is outside the allowed range"); } return value; } private BigDecimal effectiveFee(BigDecimal override, BigDecimal fallback) { BigDecimal value = override == null ? fallback : requiredMoney(override, "seriousIllnessMonthlyFee"); if (value == null || value.compareTo(ZERO) < 0 || value.compareTo(MAX_ILLNESS_FEE) > 0) { throw invalidParameter("seriousIllnessMonthlyFee is outside the allowed range"); } return value; } private BigDecimal taxFor(BigDecimal taxableIncome) { if (taxableIncome.compareTo(ZERO) <= 0) { return ZERO; } for (TaxBracket bracket : ANNUAL_BRACKETS) { if (bracket.upperBound == null || taxableIncome.compareTo(bracket.upperBound) <= 0) { return nonNegative(taxableIncome.multiply(bracket.rate).subtract(bracket.quickDeduction)); } } return ZERO; } private BigDecimal clamp(BigDecimal value, BigDecimal min, BigDecimal max) { if (value.compareTo(min) < 0) { return min; } return value.compareTo(max) > 0 ? max : value; } private void validatePolicyRange(BigDecimal min, BigDecimal max, String field) { if (min == null || max == null || min.compareTo(ZERO) < 0 || max.compareTo(min) < 0) { throw invalidParameter(field + " city range is invalid"); } } private BigDecimal requiredMoney(BigDecimal value, String field) { if (value == null || value.scale() > MONEY_SCALE) { throw invalidParameter(field + " must have at most two decimal places"); } return value; } private BigDecimal optionalMoney(BigDecimal value, String field, BigDecimal max) { BigDecimal effective = value == null ? ZERO : requiredMoney(value, field); if (effective.compareTo(ZERO) < 0 || effective.compareTo(max) > 0) { throw invalidParameter(field + " is outside the allowed range"); } return effective; } private BigDecimal requiredRate(BigDecimal value, String field) { if (value == null || value.scale() > 6) { throw invalidParameter(field + " must have at most six decimal places"); } return value; } private BigDecimal nonNegative(BigDecimal value) { return value.compareTo(ZERO) < 0 ? ZERO : value; } private BigDecimal money(BigDecimal value) { return value.setScale(MONEY_SCALE, RoundingMode.HALF_UP); } private BigDecimal rate(BigDecimal value) { return value.setScale(6, RoundingMode.HALF_UP); } private BusinessException invalidParameter(String detail) { return new BusinessException(ErrorCode.SALARY_INVALID_PARAMETER, detail); } private record TaxBracket(BigDecimal upperBound, BigDecimal rate, BigDecimal quickDeduction) { } }