Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dependencies {
implementation("tools.jackson.module:jackson-module-kotlin")

// test
testImplementation("io.mockk:mockk:1.13.12")
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
testImplementation("org.springframework.boot:spring-boot-starter-data-jpa-test")
testImplementation("org.springframework.boot:spring-boot-starter-actuator-test")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.moa.controller
import com.moa.common.auth.Auth
import com.moa.common.auth.AuthMemberInfo
import com.moa.common.response.ApiResponse
import com.moa.service.NotificationSettingService
import com.moa.service.dto.NotificationSettingUpdateRequest
import com.moa.service.notification.NotificationSettingService
import io.swagger.v3.oas.annotations.tags.Tag
import org.springframework.web.bind.annotation.*

Expand Down
7 changes: 7 additions & 0 deletions src/main/kotlin/com/moa/controller/WorkdayController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ class WorkdayController(
@PathVariable date: LocalDate,
) = ApiResponse.success(workdayService.getSchedule(member.id, date))

@GetMapping("/earnings")
fun getMonthlyEarnings(
@Auth member: AuthMemberInfo,
@RequestParam year: Int,
@RequestParam month: Int,
) = ApiResponse.success(workdayService.getMonthlyEarnings(member.id, year, month))

@GetMapping
fun getMonthlySchedules(
@Auth member: AuthMemberInfo,
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/com/moa/entity/NotificationType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ enum class NotificationType(
val title: String,
val body: String
) {
CLOCK_IN("출근 시간이에요!", "지금부터 급여가 쌓일 예정이에요."),
CLOCK_OUT("퇴근 시간이에요!", "오늘 ₩%s 벌었어요.");
CLOCK_IN("출근 했어요!", "지금부터 급여가 쌓일 예정이에요."),
CLOCK_OUT("퇴근 했어요!", "오늘 ₩%s 벌었어요.");

fun getBody(vararg args: Any): String {
return String.format(body, *args)
Expand Down
17 changes: 17 additions & 0 deletions src/main/kotlin/com/moa/entity/SalaryType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package com.moa.entity
import java.math.BigDecimal
import java.math.RoundingMode
import java.time.DayOfWeek
import java.time.Duration
import java.time.LocalDate
import java.time.LocalTime
import java.time.YearMonth

enum class SalaryType {
Expand Down Expand Up @@ -42,6 +44,21 @@ object SalaryCalculator {
return monthlySalary.divide(BigDecimal(workDaysCount), 0, RoundingMode.HALF_UP)
}

fun calculateWorkMinutes(clockIn: LocalTime, clockOut: LocalTime): Long {
val minutes = Duration.between(clockIn, clockOut).toMinutes()
return if (minutes <= 0) minutes + 24 * 60 else minutes
}

fun calculateEarnings(
dailyRate: BigDecimal,
policyWorkMinutes: Long,
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)
}

private fun getWorkDaysInPeriod(
start: LocalDate,
end: LocalDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ interface WorkPolicyVersionRepository : JpaRepository<WorkPolicyVersion, Long> {

@Query(
"""
SELECT DISTINCT w FROM WorkPolicyVersion w
JOIN FETCH w.workdays
WHERE w.effectiveFrom = (
SELECT MAX(w2.effectiveFrom)
FROM WorkPolicyVersion w2
WHERE w2.memberId = w.memberId AND w2.effectiveFrom <= :date
select distinct w from WorkPolicyVersion w
join fetch w.workdays
where w.effectiveFrom = (
select max(w2.effectiveFrom)
from WorkPolicyVersion w2
where w2.memberId = w.memberId and w2.effectiveFrom <= :date
)
"""
)
Expand Down
70 changes: 70 additions & 0 deletions src/main/kotlin/com/moa/service/EarningsCalculator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.moa.service

import com.moa.entity.DailyWorkScheduleType
import com.moa.entity.SalaryCalculator
import com.moa.entity.SalaryType
import com.moa.entity.WorkPolicyVersion
import com.moa.repository.PayrollVersionRepository
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.math.RoundingMode
import java.time.LocalDate
import java.time.LocalTime
import java.time.YearMonth

@Service
class EarningsCalculator(
private val payrollVersionRepository: PayrollVersionRepository,
) {
fun getDefaultMonthlySalary(memberId: Long, date: LocalDate): Int? {
val lastDayOfMonth = YearMonth.from(date).atEndOfMonth()
val payroll = payrollVersionRepository
.findTopByMemberIdAndEffectiveFromLessThanEqualOrderByEffectiveFromDesc(
memberId, lastDayOfMonth,
) ?: return null

return when (SalaryType.from(payroll.salaryInputType)) {
SalaryType.YEARLY -> payroll.salaryAmount.toBigDecimal()
.divide(BigDecimal(12), 0, RoundingMode.HALF_UP).toInt()
SalaryType.MONTHLY -> payroll.salaryAmount.toInt()
}
}

fun calculateDailyEarnings(
memberId: Long,
date: LocalDate,
policy: WorkPolicyVersion,
type: DailyWorkScheduleType,
clockInTime: LocalTime?,
clockOutTime: LocalTime?,
): BigDecimal? {
if (type == DailyWorkScheduleType.NONE) return BigDecimal.ZERO

val lastDayOfMonth = YearMonth.from(date).atEndOfMonth()
val payroll = payrollVersionRepository
.findTopByMemberIdAndEffectiveFromLessThanEqualOrderByEffectiveFromDesc(
memberId, lastDayOfMonth,
) ?: return null

val dailyRate = SalaryCalculator.calculateDailyRate(
targetDate = date,
salaryType = SalaryType.from(payroll.salaryInputType),
salaryAmount = payroll.salaryAmount,
workDays = policy.workdays.map { it.dayOfWeek }.toSet(),
)
if (dailyRate == BigDecimal.ZERO) return dailyRate

// 유급 휴가는 기본 일급 반환
if (type == DailyWorkScheduleType.VACATION) return dailyRate

if (clockInTime != null && clockOutTime != null) {
val policyMinutes = SalaryCalculator.calculateWorkMinutes(
policy.clockInTime, policy.clockOutTime,
)
val actualMinutes = SalaryCalculator.calculateWorkMinutes(clockInTime, clockOutTime)
return SalaryCalculator.calculateEarnings(dailyRate, policyMinutes, actualMinutes)
}

return dailyRate
}
}
Loading
Loading