You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

150 lines
6.8 KiB

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();
}
}