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
5 changes: 3 additions & 2 deletions src/main/kotlin/com/moa/entity/DailyEventType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ enum class DailyEventType {
*
* @param date 이벤트를 판정할 기준 일자
* @param paydayDay 사용자 설정 급여일
* @param publicHolidays 해당 월의 공휴일 날짜 집합
* @param publicHolidays 급여일 보정과 공휴일 이벤트 판정에 필요한 공휴일 날짜 집합.
* date가 속한 달과 다음 달의 공휴일을 포함해야 합니다.
* @return 해당 일자에 적용되는 [DailyEventType] 목록
*/
fun resolve(
Expand All @@ -28,7 +29,7 @@ enum class DailyEventType {
): List<DailyEventType> {
val events = mutableListOf<DailyEventType>()

if (paydayDay.isPayday(date)) {
if (paydayDay.isPayday(date, publicHolidays)) {
Comment thread
jeyongsong marked this conversation as resolved.
events += PAYDAY
}
if (date in publicHolidays) {
Expand Down
56 changes: 44 additions & 12 deletions src/main/kotlin/com/moa/entity/PaydayDay.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import java.time.YearMonth
* 사용자가 설정한 급여일을 표현하는 값 객체입니다.
*
* 급여일은 1일부터 31일 사이의 값만 허용하며,
* 실제 급여일 계산 시 월말 보정과 주말 보정 규칙을 함께 제공합니다.
* 실제 급여일 계산 시 월말 보정과 휴일 보정 규칙을 함께 제공합니다.
*/
@Embeddable
data class PaydayDay(
Expand All @@ -25,40 +25,72 @@ data class PaydayDay(
* 특정 연월에 적용되는 실제 급여일을 계산합니다.
*
* 설정한 급여일이 해당 월에 없으면 말일로 보정하고,
* 보정된 날짜가 주말이면 직전 금요일로 당깁니다.
* 보정된 날짜가 주말 또는 공휴일이면 직전 영업일로 당깁니다.
*
* @param year 급여일을 계산할 연도
* @param month 급여일을 계산할 월
* @param publicHolidays 해당 연월의 급여일 보정에 사용할 공휴일 날짜 집합
* @return 실제 적용되는 급여일
*/
fun resolveEffectiveDate(year: Int, month: Int): LocalDate {
fun resolveEffectiveDate(
year: Int,
month: Int,
publicHolidays: Set<LocalDate> = emptySet(),
): LocalDate {
val yearMonth = YearMonth.of(year, month)
val baseDate = yearMonth.atDay(minOf(value, yearMonth.lengthOfMonth()))
var date = yearMonth.atDay(minOf(value, yearMonth.lengthOfMonth()))

return when (baseDate.dayOfWeek) {
DayOfWeek.SATURDAY -> baseDate.minusDays(1)
DayOfWeek.SUNDAY -> baseDate.minusDays(2)
else -> baseDate
while (date.isHoliday(publicHolidays)) {
date = date.minusDays(1)
}

return date
}

/**
* 특정 날짜가 이 급여일 설정의 실제 급여일인지 판정합니다.
*
* @param date 판정할 날짜
* @param publicHolidays 급여일 보정에 필요한 공휴일 날짜 집합.
* date가 속한 달과 다음 달의 공휴일을 포함해야 합니다.
* @return 해당 날짜가 실제 급여일이면 `true`
*/
fun isPayday(date: LocalDate): Boolean =
resolveEffectiveDate(date.year, date.monthValue) == date
fun isPayday(
date: LocalDate,
publicHolidays: Set<LocalDate> = emptySet(),
): Boolean {
val currentMonth = YearMonth.from(date)
val candidateMonths = setOf(currentMonth, currentMonth.plusMonths(1))

return candidateMonths.any {
resolveEffectiveDate(it.year, it.monthValue, publicHolidays) == date
Comment thread
jeyongsong marked this conversation as resolved.
}
}

companion object {
/**
* 특정 날짜에 실제 급여일로 귀결되는 모든 급여일 설정 값을 반환합니다.
*
* @param date 기준 날짜
* @param publicHolidays 급여일 보정에 필요한 공휴일 날짜 집합.
* date가 속한 달과 다음 달의 공휴일을 포함해야 합니다.
* @return 해당 날짜를 실제 급여일로 가지는 [PaydayDay] 집합
*/
fun resolvingTo(date: LocalDate): Set<PaydayDay> =
(1..31).map(::PaydayDay).filterTo(mutableSetOf()) { it.isPayday(date) }
fun resolvingTo(
date: LocalDate,
publicHolidays: Set<LocalDate> = emptySet(),
): Set<PaydayDay> {
val currentMonth = YearMonth.from(date)
val candidateMonths = setOf(currentMonth, currentMonth.plusMonths(1))

return (1..31).map(::PaydayDay).filterTo(mutableSetOf()) { paydayDay ->
candidateMonths.any {
paydayDay.resolveEffectiveDate(it.year, it.monthValue, publicHolidays) == date
Comment thread
jeyongsong marked this conversation as resolved.
}
}
}
}

private fun LocalDate.isHoliday(publicHolidays: Set<LocalDate>): Boolean =
dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY || this in publicHolidays
}
10 changes: 10 additions & 0 deletions src/main/kotlin/com/moa/service/PublicHolidayService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ class PublicHolidayService(
.toSet()
}

@Transactional(readOnly = true)
fun getHolidayDatesForPaydayResolution(date: LocalDate): Set<LocalDate> {
val start = date.withDayOfMonth(1)
val nextMonth = start.plusMonths(1)
val end = nextMonth.withDayOfMonth(nextMonth.lengthOfMonth())
return publicHolidayRepository.findAllByDateBetween(start, end)
.map { it.date }
.toSet()
}

@Transactional(readOnly = true)
fun isHoliday(date: LocalDate): Boolean =
publicHolidayRepository.existsByDate(date)
Expand Down
8 changes: 4 additions & 4 deletions src/main/kotlin/com/moa/service/WorkdayService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class WorkdayService(

val monthlyPolicy = resolveMonthlyRepresentativePolicyOrNull(memberId, year, month)
val paydayDay = resolvePaydayDay(memberId)
val publicHolidays = publicHolidayService.getHolidayDatesForMonth(year, month)
val publicHolidays = publicHolidayService.getHolidayDatesForPaydayResolution(start)

return generateSequence(start) { it.plusDays(1) }
.takeWhile { !it.isAfter(end) }
Expand Down Expand Up @@ -156,7 +156,7 @@ class WorkdayService(
): WorkdayResponse {
val saved = dailyWorkScheduleRepository.findByMemberIdAndDate(memberId, date)
val policy = resolveMonthlyRepresentativePolicyOrNull(memberId, date.year, date.monthValue)
val publicHolidays = publicHolidayService.getHolidayDatesForMonth(date.year,date.monthValue)
val publicHolidays = publicHolidayService.getHolidayDatesForPaydayResolution(date)
val schedule = resolveSchedule(saved, policy, date, publicHolidays)
return createWorkdayResponse(memberId, date, schedule, policy, resolvePaydayDay(memberId), publicHolidays)
}
Expand Down Expand Up @@ -201,7 +201,7 @@ class WorkdayService(
)

val schedule = ResolvedSchedule(savedSchedule.type, savedSchedule.clockInTime, savedSchedule.clockOutTime)
val publicHolidays = if (publicHolidayService.isHoliday(date)) setOf(date) else emptySet()
val publicHolidays = publicHolidayService.getHolidayDatesForPaydayResolution(date)
return createWorkdayResponse(
memberId,
date,
Expand Down Expand Up @@ -242,7 +242,7 @@ class WorkdayService(
)

val schedule = ResolvedSchedule(savedSchedule.type, savedSchedule.clockInTime, savedSchedule.clockOutTime)
val publicHolidays = if (publicHolidayService.isHoliday(date)) setOf(date) else emptySet()
val publicHolidays = publicHolidayService.getHolidayDatesForPaydayResolution(date)
return createWorkdayResponse(
memberId,
date,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.moa.entity.notification.NotificationSettingType
import com.moa.entity.notification.NotificationType
import com.moa.repository.NotificationLogRepository
import com.moa.repository.ProfileRepository
import com.moa.service.PublicHolidayService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
Expand All @@ -18,6 +19,7 @@ class PaydayNotificationBatchService(
private val profileRepository: ProfileRepository,
private val notificationLogRepository: NotificationLogRepository,
private val notificationEligibilityService: NotificationEligibilityService,
private val publicHolidayService: PublicHolidayService,
) {
private val log = LoggerFactory.getLogger(javaClass)

Expand Down Expand Up @@ -49,7 +51,8 @@ class PaydayNotificationBatchService(
}

private fun findPaydayProfiles(date: LocalDate): List<Profile> {
val candidatePaydayDays = PaydayDay.resolvingTo(date)
val publicHolidays = publicHolidayService.getHolidayDatesForPaydayResolution(date)
val candidatePaydayDays = PaydayDay.resolvingTo(date, publicHolidays)
.map { it.value }

if (candidatePaydayDays.isEmpty()) {
Expand Down
16 changes: 14 additions & 2 deletions src/test/kotlin/com/moa/entity/DailyEventTypeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ class DailyEventTypeTest {
assertThat(result).containsExactly(DailyEventType.PAYDAY)
}

@Test
fun `설정한 급여일이 공휴일이면 직전 영업일을 급여일로 간주한다`() {
val holiday = LocalDate.of(2025, 6, 25)
val result = DailyEventType.resolve(
date = LocalDate.of(2025, 6, 24),
paydayDay = PaydayDay(25),
publicHolidays = setOf(holiday),
)

assertThat(result).containsExactly(DailyEventType.PAYDAY)
}

@Test
fun `공휴일이면 PUBLIC_HOLIDAY 이벤트를 반환한다`() {
val holiday = LocalDate.of(2025, 5, 5)
Expand All @@ -74,15 +86,15 @@ class DailyEventTypeTest {
}

@Test
fun `급여일과 공휴일이 겹치면 두 이벤트 모두 반환한다`() {
fun `설정한 급여일이 공휴일이면 해당 날짜는 공휴일 이벤트만 반환한다`() {
val date = LocalDate.of(2025, 6, 25)
val result = DailyEventType.resolve(
date = date,
paydayDay = PaydayDay(25),
publicHolidays = setOf(date),
)

assertThat(result).containsExactly(DailyEventType.PAYDAY, DailyEventType.PUBLIC_HOLIDAY)
assertThat(result).containsExactly(DailyEventType.PUBLIC_HOLIDAY)
}

@Test
Expand Down
34 changes: 34 additions & 0 deletions src/test/kotlin/com/moa/entity/PaydayDayTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,45 @@ class PaydayDayTest {
assertThat(result).isEqualTo(LocalDate.of(2025, 5, 30))
}

@Test
fun `실제 급여일이 공휴일이면 직전 영업일로 당긴다`() {
val holidays = setOf(LocalDate.of(2025, 6, 25))

val result = PaydayDay(25).resolveEffectiveDate(2025, 6, holidays)

assertThat(result).isEqualTo(LocalDate.of(2025, 6, 24))
}

@Test
fun `주말과 공휴일이 연속되면 직전 영업일까지 반복해서 당긴다`() {
val holidays = setOf(LocalDate.of(2025, 5, 30))

val result = PaydayDay(31).resolveEffectiveDate(2025, 5, holidays)

assertThat(result).isEqualTo(LocalDate.of(2025, 5, 29))
}

@Test
fun `특정 날짜가 실제 급여일인지 판정할 수 있다`() {
val paydayDay = PaydayDay(25)

assertThat(paydayDay.isPayday(LocalDate.of(2025, 6, 25))).isTrue()
assertThat(paydayDay.isPayday(LocalDate.of(2025, 6, 24))).isFalse()
}

@Test
fun `다음 달 초 급여일이 전월 말로 당겨진 날짜도 급여일로 판정한다`() {
val paydayDay = PaydayDay(1)

assertThat(paydayDay.isPayday(LocalDate.of(2025, 5, 30))).isTrue()
}

@Test
fun `특정 날짜로 당겨지는 급여일 설정값을 공휴일까지 고려해 찾는다`() {
val holidays = setOf(LocalDate.of(2025, 6, 25))

val result = PaydayDay.resolvingTo(LocalDate.of(2025, 6, 24), holidays)

assertThat(result).contains(PaydayDay(25))
}
}
Loading