From cd5d810bb0eec4e2ae029e3f670a81365fbda099 Mon Sep 17 00:00:00 2001 From: jeyong Date: Fri, 3 Apr 2026 18:34:59 +0900 Subject: [PATCH 1/2] Refactor earnings calculations in WorkdayService and CompensationCalculator to use BigDecimal for precision and apply consistent rounding --- .../kotlin/com/moa/service/WorkdayService.kt | 11 ++--- .../calculator/CompensationCalculator.kt | 10 +++-- .../calculator/CompensationCalculatorTest.kt | 42 +++++++++++++++---- 3 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/com/moa/service/WorkdayService.kt b/src/main/kotlin/com/moa/service/WorkdayService.kt index 0680894..a5fb71c 100644 --- a/src/main/kotlin/com/moa/service/WorkdayService.kt +++ b/src/main/kotlin/com/moa/service/WorkdayService.kt @@ -14,6 +14,7 @@ import com.moa.service.notification.NotificationSyncService import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.math.BigDecimal +import java.math.RoundingMode import java.time.LocalDate import java.time.LocalTime import java.time.YearMonth @@ -77,7 +78,7 @@ class WorkdayService( .findAllByMemberIdAndDateBetween(memberId, start, lastCalculableDate) .associateBy { it.date } - var workedEarnings = 0L + var workedEarnings = BigDecimal.ZERO var workedMinutes = 0L var date = start while (!date.isAfter(lastCalculableDate)) { @@ -95,21 +96,21 @@ class WorkdayService( completedWork.clockIn, completedWork.clockOut, ) - workedEarnings += calculateDailyEarnings( + workedEarnings = workedEarnings.add(calculateDailyEarnings( memberId, date, monthlyPolicy, completedWork.type, completedWork.clockIn, completedWork.clockOut, - ).toLong() + )) } date = date.plusDays(1) } return MonthlyEarningsResponse( - workedEarnings = workedEarnings, + workedEarnings = workedEarnings.setScale(0, RoundingMode.HALF_UP).toLong(), standardSalary = standardSalary, workedMinutes = workedMinutes, standardMinutes = standardMinutes, @@ -389,7 +390,7 @@ class WorkdayService( return calculateDailyEarnings( memberId, date, policy, schedule.type, schedule.clockIn, schedule.clockOut, - ).toInt() + ).setScale(0, RoundingMode.HALF_UP).toInt() } /** diff --git a/src/main/kotlin/com/moa/service/calculator/CompensationCalculator.kt b/src/main/kotlin/com/moa/service/calculator/CompensationCalculator.kt index 7ce39cb..4e03c94 100644 --- a/src/main/kotlin/com/moa/service/calculator/CompensationCalculator.kt +++ b/src/main/kotlin/com/moa/service/calculator/CompensationCalculator.kt @@ -16,6 +16,10 @@ import java.time.* */ @Service class CompensationCalculator { + companion object { + private const val MONEY_SCALE = 10 + } + /** * 주어진 기간의 기준 근무 시간을 계산합니다. * @@ -151,7 +155,7 @@ class CompensationCalculator { if (workDaysCount == 0) return BigDecimal.ZERO - return monthlySalary.divide(BigDecimal(workDaysCount), 0, RoundingMode.HALF_UP) + return monthlySalary.divide(BigDecimal(workDaysCount), MONEY_SCALE, RoundingMode.HALF_UP) } /** @@ -171,8 +175,8 @@ class CompensationCalculator { actualWorkMinutes: Long, ): BigDecimal { if (policyWorkMinutes <= 0) return dailyRate - val minuteRate = dailyRate.divide(BigDecimal(policyWorkMinutes), 10, RoundingMode.HALF_UP) - return minuteRate.multiply(BigDecimal(actualWorkMinutes)).setScale(0, RoundingMode.HALF_UP) + val minuteRate = dailyRate.divide(BigDecimal(policyWorkMinutes), MONEY_SCALE, RoundingMode.HALF_UP) + return minuteRate.multiply(BigDecimal(actualWorkMinutes)) } /** diff --git a/src/test/kotlin/com/moa/service/calculator/CompensationCalculatorTest.kt b/src/test/kotlin/com/moa/service/calculator/CompensationCalculatorTest.kt index ddb1b4a..6e7eb54 100644 --- a/src/test/kotlin/com/moa/service/calculator/CompensationCalculatorTest.kt +++ b/src/test/kotlin/com/moa/service/calculator/CompensationCalculatorTest.kt @@ -7,6 +7,7 @@ import com.moa.entity.Workday import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import java.math.BigDecimal +import java.math.RoundingMode import java.time.DayOfWeek import java.time.LocalDate import java.time.LocalTime @@ -97,7 +98,7 @@ class CompensationCalculatorTest { clockOutTime = LocalTime.of(18, 0), ) - assertThat(result.toLong()).isEqualTo(142857L) + assertThat(result).isEqualByComparingTo(BigDecimal("142857.1428571620")) } @Test @@ -127,7 +128,7 @@ class CompensationCalculatorTest { clockOutTime = LocalTime.of(18, 0), ) - assertThat(result.toLong()).isEqualTo(142857L) + assertThat(result).isEqualByComparingTo(BigDecimal("142857.1428571620")) } @Test @@ -189,7 +190,7 @@ class CompensationCalculatorTest { clockOutTime = null, ) - assertThat(result.toLong()).isEqualTo(142857L) + assertThat(result).isEqualByComparingTo(BigDecimal("142857.1428571429")) } @Test @@ -207,7 +208,7 @@ class CompensationCalculatorTest { ), ) - assertThat(result).isEqualByComparingTo(BigDecimal("142857")) + assertThat(result).isEqualByComparingTo(BigDecimal("142857.1428571429")) } @Test @@ -235,7 +236,7 @@ class CompensationCalculatorTest { ), ) - assertThat(result).isEqualByComparingTo(BigDecimal("230769")) + assertThat(result).isEqualByComparingTo(BigDecimal("230769.2307692308")) } @Test @@ -251,7 +252,7 @@ class CompensationCalculatorTest { ), ) - assertThat(result.scale()).isEqualTo(0) + assertThat(result.scale()).isGreaterThan(0) } @Test @@ -290,7 +291,7 @@ class CompensationCalculatorTest { val result = compensationCalculator.calculateEarnings(dailyRate, 540, 540) - assertThat(result).isEqualByComparingTo(BigDecimal("100000")) + assertThat(result).isEqualByComparingTo(BigDecimal("100000.0000000080")) } @Test @@ -299,7 +300,7 @@ class CompensationCalculatorTest { val result = compensationCalculator.calculateEarnings(dailyRate, 540, 600) - assertThat(result).isEqualByComparingTo(BigDecimal("111111")) + assertThat(result).isEqualByComparingTo(BigDecimal("111111.1111111200")) } @Test @@ -308,7 +309,30 @@ class CompensationCalculatorTest { val result = compensationCalculator.calculateEarnings(dailyRate, 540, 480) - assertThat(result).isEqualByComparingTo(BigDecimal("88889")) + assertThat(result).isEqualByComparingTo(BigDecimal("88888.8888888960")) + } + + @Test + fun `월급을 일별 반올림하지 않고 월 합계에서만 반올림하면 월 총액이 유지된다`() { + val dailyRate = compensationCalculator.calculateDailyRate( + targetDate = LocalDate.of(2025, 6, 1), + salaryType = SalaryInputType.MONTHLY, + salaryAmount = 3_000_000, + workDays = setOf( + DayOfWeek.MONDAY, + DayOfWeek.TUESDAY, + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY, + ), + ) + + val monthlyTotal = (1..21).fold(BigDecimal.ZERO) { acc, _ -> + acc.add(compensationCalculator.calculateEarnings(dailyRate, 540, 540)) + } + + assertThat(monthlyTotal.setScale(0, RoundingMode.HALF_UP)) + .isEqualByComparingTo(BigDecimal("3000000")) } @Test From 1df3762181be825d381181ceb5f146266df783c0 Mon Sep 17 00:00:00 2001 From: jeyong Date: Fri, 3 Apr 2026 18:42:42 +0900 Subject: [PATCH 2/2] Refactor earnings calculation logic in CompensationCalculator to handle exact policy work minutes and update test cases for accuracy --- .../moa/service/calculator/CompensationCalculator.kt | 1 + .../service/calculator/CompensationCalculatorTest.kt | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/moa/service/calculator/CompensationCalculator.kt b/src/main/kotlin/com/moa/service/calculator/CompensationCalculator.kt index 4e03c94..916c7f2 100644 --- a/src/main/kotlin/com/moa/service/calculator/CompensationCalculator.kt +++ b/src/main/kotlin/com/moa/service/calculator/CompensationCalculator.kt @@ -175,6 +175,7 @@ class CompensationCalculator { actualWorkMinutes: Long, ): BigDecimal { if (policyWorkMinutes <= 0) return dailyRate + if (actualWorkMinutes == policyWorkMinutes) return dailyRate val minuteRate = dailyRate.divide(BigDecimal(policyWorkMinutes), MONEY_SCALE, RoundingMode.HALF_UP) return minuteRate.multiply(BigDecimal(actualWorkMinutes)) } diff --git a/src/test/kotlin/com/moa/service/calculator/CompensationCalculatorTest.kt b/src/test/kotlin/com/moa/service/calculator/CompensationCalculatorTest.kt index 6e7eb54..4cf4235 100644 --- a/src/test/kotlin/com/moa/service/calculator/CompensationCalculatorTest.kt +++ b/src/test/kotlin/com/moa/service/calculator/CompensationCalculatorTest.kt @@ -87,7 +87,7 @@ class CompensationCalculatorTest { } @Test - fun `휴무이면 저장된 시간 기반으로 급여를 계산한다`() { + fun `휴무가 정책 근무시간과 같으면 기준 일급과 동일한 급여를 계산한다`() { val result = compensationCalculator.calculateDailyEarnings( date = DATE, salaryType = SalaryInputType.MONTHLY, @@ -98,7 +98,7 @@ class CompensationCalculatorTest { clockOutTime = LocalTime.of(18, 0), ) - assertThat(result).isEqualByComparingTo(BigDecimal("142857.1428571620")) + assertThat(result).isEqualByComparingTo(BigDecimal("142857.1428571429")) } @Test @@ -117,7 +117,7 @@ class CompensationCalculatorTest { } @Test - fun `정상 근무시 일급을 반환한다`() { + fun `정상 근무시 기준 일급을 반환한다`() { val result = compensationCalculator.calculateDailyEarnings( date = DATE, salaryType = SalaryInputType.MONTHLY, @@ -128,7 +128,7 @@ class CompensationCalculatorTest { clockOutTime = LocalTime.of(18, 0), ) - assertThat(result).isEqualByComparingTo(BigDecimal("142857.1428571620")) + assertThat(result).isEqualByComparingTo(BigDecimal("142857.1428571429")) } @Test @@ -240,7 +240,7 @@ class CompensationCalculatorTest { } @Test - fun `일급 계산 결과의 소수점은 반올림한다`() { + fun `일급 계산 결과는 월 합산 전까지 소수점을 유지한다`() { val result = compensationCalculator.calculateDailyRate( targetDate = LocalDate.of(2025, 6, 1), salaryType = SalaryInputType.MONTHLY, @@ -291,7 +291,7 @@ class CompensationCalculatorTest { val result = compensationCalculator.calculateEarnings(dailyRate, 540, 540) - assertThat(result).isEqualByComparingTo(BigDecimal("100000.0000000080")) + assertThat(result).isEqualByComparingTo(BigDecimal("100000")) } @Test