package com.mydreamjob.salarycalculator; import com.mydreamjob.common.exception.BusinessException; import com.mydreamjob.common.exception.ErrorCode; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.time.LocalDate; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class SalaryCalculationEngineTest { private final SalaryCalculationEngine engine = new SalaryCalculationEngine(); @Test void calculatesBaseCaseWithCumulativeWithholding() { SalaryCalculationResult result = engine.calculate(input("10000", 1, "0", null), policy()); assertThat(result.getSocialSecurityBase()).isEqualByComparingTo("10000.00"); assertThat(result.getHousingFundBase()).isEqualByComparingTo("10000.00"); assertThat(result.getPension()).isEqualByComparingTo("800.00"); assertThat(result.getMedical()).isEqualByComparingTo("200.00"); assertThat(result.getUnemployment()).isEqualByComparingTo("50.00"); assertThat(result.getHousingFund()).isEqualByComparingTo("1200.00"); assertThat(result.getCurrentMonthTax()).isEqualByComparingTo("82.50"); assertThat(result.getNetSalary()).isEqualByComparingTo("7667.50"); assertThat(result.isEstimate()).isTrue(); assertThat(result.getDataLevel()).isEqualTo(SalaryDataLevel.CITY); } @Test void clampsBothBasesToFloorAndCeiling() { SalaryCalculationResult low = engine.calculate(input("3000", 1, "0", null), policy()); SalaryCalculationResult high = engine.calculate(input("30000", 1, "0", null), policy()); assertThat(low.getSocialSecurityBase()).isEqualByComparingTo("5000.00"); assertThat(low.getHousingFundBase()).isEqualByComparingTo("5000.00"); assertThat(low.getCurrentMonthTax()).isEqualByComparingTo("0.00"); assertThat(low.getNetSalary()).isEqualByComparingTo("1875.00"); assertThat(high.getSocialSecurityBase()).isEqualByComparingTo("20000.00"); assertThat(high.getHousingFundBase()).isEqualByComparingTo("20000.00"); assertThat(high.getCurrentMonthTax()).isEqualByComparingTo("615.00"); assertThat(high.getNetSalary()).isEqualByComparingTo("24885.00"); } @Test void usesPriorCumulativeTaxForTwelfthMonth() { SalaryCalculationResult result = engine.calculate(input("20000", 12, "0", null), policy()); assertThat(result.getCumulativeTaxableIncome()).isEqualByComparingTo("126000.00"); assertThat(result.getCumulativeTaxDue()).isEqualByComparingTo("10080.00"); assertThat(result.getPreviouslyWithheldTax()).isEqualByComparingTo("9030.00"); assertThat(result.getCurrentMonthTax()).isEqualByComparingTo("1050.00"); assertThat(result.getNetSalary()).isEqualByComparingTo("14450.00"); } @Test void appliesSpecialDeductionAndOverrides() { SalaryOverrideParameters overrides = SalaryOverrideParameters.builder() .socialSecurityBase(new BigDecimal("8000.00")) .housingFundBase(new BigDecimal("10000.00")) .pensionRate(new BigDecimal("0.08")) .medicalRate(new BigDecimal("0.02")) .unemploymentRate(new BigDecimal("0.005")) .housingFundRate(new BigDecimal("0.12")) .seriousIllnessMonthlyFee(BigDecimal.ZERO) .build(); SalaryCalculationResult result = engine.calculate( input("10000", 1, "2000", overrides), policy()); assertThat(result.getSocialSecurityBase()).isEqualByComparingTo("8000.00"); assertThat(result.getPension()).isEqualByComparingTo("640.00"); assertThat(result.getCurrentMonthTax()).isEqualByComparingTo("28.80"); assertThat(result.getNetSalary()).isEqualByComparingTo("7931.20"); } @Test void rejectsOverrideOutsideCityBaseRange() { SalaryOverrideParameters overrides = SalaryOverrideParameters.builder() .socialSecurityBase(new BigDecimal("4999.99")) .build(); assertThatThrownBy(() -> engine.calculate(input("10000", 1, "0", overrides), policy())) .isInstanceOf(BusinessException.class) .extracting(ex -> ((BusinessException) ex).getErrorCodeInt()) .isEqualTo(ErrorCode.SALARY_INVALID_PARAMETER.getCode()); } @Test void zeroContributionRateAllowsZeroBase() { SalaryOverrideParameters overrides = SalaryOverrideParameters.builder() .socialSecurityBase(BigDecimal.ZERO) .housingFundBase(BigDecimal.ZERO) .pensionRate(BigDecimal.ZERO) .medicalRate(BigDecimal.ZERO) .unemploymentRate(BigDecimal.ZERO) .housingFundRate(BigDecimal.ZERO) .seriousIllnessMonthlyFee(BigDecimal.ZERO) .build(); SalaryCalculationResult result = engine.calculate( input("10000", 1, "0", overrides), policy()); assertThat(result.getPension()).isEqualByComparingTo("0.00"); assertThat(result.getMedical()).isEqualByComparingTo("0.00"); assertThat(result.getUnemployment()).isEqualByComparingTo("0.00"); assertThat(result.getHousingFund()).isEqualByComparingTo("0.00"); assertThat(result.getCurrentMonthTax()).isEqualByComparingTo("150.00"); assertThat(result.getNetSalary()).isEqualByComparingTo("9850.00"); } private SalaryCalculationInput input( String gross, int monthIndex, String specialDeduction, SalaryOverrideParameters overrides) { return SalaryCalculationInput.builder() .grossMonthlySalary(new BigDecimal(gross)) .monthIndex(monthIndex) .specialAdditionalDeduction(new BigDecimal(specialDeduction)) .overrides(overrides) .build(); } private SalaryCityPolicy policy() { return SalaryCityPolicy.builder() .cityCode("110000") .provinceCode("110000") .provinceName("北京市") .cityName("北京市") .policyVersion("2026-09-20-v1") .policyEffectiveDate(LocalDate.of(2026, 7, 1)) .socialSecurityBaseMin(new BigDecimal("5000.00")) .socialSecurityBaseMax(new BigDecimal("20000.00")) .housingFundBaseMin(new BigDecimal("5000.00")) .housingFundBaseMax(new BigDecimal("20000.00")) .pensionRate(new BigDecimal("0.080000")) .medicalRate(new BigDecimal("0.020000")) .unemploymentRate(new BigDecimal("0.005000")) .housingFundRate(new BigDecimal("0.120000")) .seriousIllnessMonthlyFee(BigDecimal.ZERO) .dataLevel(SalaryDataLevel.CITY) .enabled(true) .build(); } }