From e9920f23286c3dbfb055ecf00b7eb5bd8de0ac1f Mon Sep 17 00:00:00 2001 From: hod Date: Thu, 9 Apr 2026 23:31:51 +0900 Subject: [PATCH 1/2] Refactor notification generation logic to prevent duplicate notifications for public holidays and paydays --- .../repository/NotificationLogRepository.kt | 16 ++++----- .../notification/NotificationBatchService.kt | 35 ++++++++++--------- .../PaydayNotificationBatchService.kt | 20 +++++------ 3 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt b/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt index 05d32b9..fc154f7 100644 --- a/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt +++ b/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt @@ -4,6 +4,7 @@ import com.moa.entity.notification.NotificationLog import com.moa.entity.notification.NotificationStatus import com.moa.entity.notification.NotificationType import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query import java.time.LocalDate import java.time.LocalTime @@ -33,14 +34,13 @@ interface NotificationLogRepository : JpaRepository { notificationTypes: Collection, ): List - fun existsByMemberIdAndScheduledDateAndStatus( - memberId: Long, - scheduledDate: LocalDate, - status: NotificationStatus, - ): Boolean - - fun existsByScheduledDateAndNotificationType( + @Query( + "select n.memberId " + + "from NotificationLog n " + + "where n.scheduledDate = :scheduledDate and n.notificationType = :notificationType" + ) + fun findMemberIdsByScheduledDateAndNotificationType( scheduledDate: LocalDate, notificationType: NotificationType, - ): Boolean + ): List } diff --git a/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt b/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt index d6e314b..69b6116 100644 --- a/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt +++ b/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt @@ -1,7 +1,7 @@ package com.moa.service.notification -import com.moa.entity.Workday import com.moa.entity.WorkPolicyVersion +import com.moa.entity.Workday import com.moa.entity.notification.NotificationLog import com.moa.entity.notification.NotificationSettingType import com.moa.entity.notification.NotificationType @@ -30,8 +30,6 @@ class NotificationBatchService( @Transactional fun generateNotificationsForDate(date: LocalDate) { - if (isAlreadyGenerated(date)) return - if (publicHolidayService.isHoliday(date)) { generateHolidayNotifications(date) } else { @@ -43,13 +41,20 @@ class NotificationBatchService( val allPolicies = workPolicyVersionRepository.findLatestEffectivePoliciesPerMember(date) if (allPolicies.isEmpty()) return - val memberIds = allPolicies.map { it.memberId } + val alreadyGeneratedMemberIds = notificationLogRepository + .findMemberIdsByScheduledDateAndNotificationType(date, NotificationType.PUBLIC_HOLIDAY) + .toSet() + + val targetPolicies = allPolicies.filter { it.memberId !in alreadyGeneratedMemberIds } + if (targetPolicies.isEmpty()) return + + val memberIds = targetPolicies.map { it.memberId } val requiredTermCodes = notificationEligibilityService.findRequiredTermCodes() val context = notificationEligibilityService.loadContext(memberIds, date) log.info("Generating public holiday notifications for {} members on {}", memberIds.size, date) - val notifications = allPolicies.mapNotNull { policy -> + val notifications = targetPolicies.mapNotNull { policy -> createHolidayNotificationIfEligible(policy.memberId, date, requiredTermCodes, context) } @@ -74,13 +79,20 @@ class NotificationBatchService( val workdayPolicies = findWorkdayPolicies(date) if (workdayPolicies.isEmpty()) return - val memberIds = workdayPolicies.map { it.memberId } + val alreadyGeneratedMemberIds = notificationLogRepository + .findMemberIdsByScheduledDateAndNotificationType(date, NotificationType.CLOCK_IN) + .toSet() + + val targetPolicies = workdayPolicies.filter { it.memberId !in alreadyGeneratedMemberIds } + if (targetPolicies.isEmpty()) return + + val memberIds = targetPolicies.map { it.memberId } val requiredTermCodes = notificationEligibilityService.findRequiredTermCodes() val context = notificationEligibilityService.loadContext(memberIds, date) log.info("Generating notifications for {} members on {}", memberIds.size, date) - val notifications = workdayPolicies.mapNotNull { policy -> + val notifications = targetPolicies.mapNotNull { policy -> createNotificationsIfEligible(policy, date, requiredTermCodes, context) }.flatten() @@ -88,15 +100,6 @@ class NotificationBatchService( log.info("Created {} notification logs for {}", notifications.size, date) } - private fun isAlreadyGenerated(date: LocalDate): Boolean { - val exists = notificationLogRepository - .existsByScheduledDateAndNotificationType(date, NotificationType.CLOCK_IN) - || notificationLogRepository - .existsByScheduledDateAndNotificationType(date, NotificationType.PUBLIC_HOLIDAY) - if (exists) log.info("Notifications already generated for date: {}", date) - return exists - } - private fun findWorkdayPolicies(date: LocalDate): List { val todayWorkday = Workday.from(date) return workPolicyVersionRepository.findLatestEffectivePoliciesPerMember(date) diff --git a/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt b/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt index fba159d..8dee781 100644 --- a/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt +++ b/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt @@ -23,18 +23,23 @@ class PaydayNotificationBatchService( @Transactional fun generateNotificationsForDate(date: LocalDate) { - if (isAlreadyGenerated(date)) return - val profiles = findPaydayProfiles(date) if (profiles.isEmpty()) return - val memberIds = profiles.map { it.memberId } + val alreadyGeneratedMemberIds = notificationLogRepository + .findMemberIdsByScheduledDateAndNotificationType(date, NotificationType.PAYDAY) + .toSet() + + val targetProfiles = profiles.filter { it.memberId !in alreadyGeneratedMemberIds } + if (targetProfiles.isEmpty()) return + + val memberIds = targetProfiles.map { it.memberId } val requiredTermCodes = notificationEligibilityService.findRequiredTermCodes() val context = notificationEligibilityService.loadContext(memberIds) log.info("Generating payday notifications for {} members on {}", memberIds.size, date) - val notifications = profiles.mapNotNull { profile -> + val notifications = targetProfiles.mapNotNull { profile -> createNotificationIfEligible(profile.memberId, date, requiredTermCodes, context) } @@ -42,13 +47,6 @@ class PaydayNotificationBatchService( log.info("Created {} payday notification logs for {}", notifications.size, date) } - private fun isAlreadyGenerated(date: LocalDate): Boolean { - val exists = notificationLogRepository - .existsByScheduledDateAndNotificationType(date, NotificationType.PAYDAY) - if (exists) log.info("Payday notifications already generated for date: {}", date) - return exists - } - private fun findPaydayProfiles(date: LocalDate): List { val candidatePaydayDays = PaydayDay.resolvingTo(date) .map { it.value } From ddb0e703bf82ebc4fdf43c56f8091a0bc7666874 Mon Sep 17 00:00:00 2001 From: hod Date: Thu, 9 Apr 2026 23:50:33 +0900 Subject: [PATCH 2/2] Refactor notification log queries to prevent duplicate notifications for public holidays and paydays --- .../com/moa/repository/NotificationLogRepository.kt | 9 ++++++--- .../moa/service/notification/NotificationBatchService.kt | 6 ++++-- .../notification/PaydayNotificationBatchService.kt | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt b/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt index fc154f7..07fc84b 100644 --- a/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt +++ b/src/main/kotlin/com/moa/repository/NotificationLogRepository.kt @@ -35,12 +35,15 @@ interface NotificationLogRepository : JpaRepository { ): List @Query( - "select n.memberId " + + "select distinct n.memberId " + "from NotificationLog n " + - "where n.scheduledDate = :scheduledDate and n.notificationType = :notificationType" + "where n.scheduledDate = :scheduledDate " + + "and n.notificationType = :notificationType " + + "and n.memberId in :memberIds" ) - fun findMemberIdsByScheduledDateAndNotificationType( + fun findMemberIdsByScheduledDateAndNotificationTypeAndMemberIdIn( scheduledDate: LocalDate, notificationType: NotificationType, + memberIds: Collection, ): List } diff --git a/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt b/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt index 69b6116..63a8d1c 100644 --- a/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt +++ b/src/main/kotlin/com/moa/service/notification/NotificationBatchService.kt @@ -41,8 +41,9 @@ class NotificationBatchService( val allPolicies = workPolicyVersionRepository.findLatestEffectivePoliciesPerMember(date) if (allPolicies.isEmpty()) return + val allMemberIds = allPolicies.map { it.memberId } val alreadyGeneratedMemberIds = notificationLogRepository - .findMemberIdsByScheduledDateAndNotificationType(date, NotificationType.PUBLIC_HOLIDAY) + .findMemberIdsByScheduledDateAndNotificationTypeAndMemberIdIn(date, NotificationType.PUBLIC_HOLIDAY, allMemberIds) .toSet() val targetPolicies = allPolicies.filter { it.memberId !in alreadyGeneratedMemberIds } @@ -79,8 +80,9 @@ class NotificationBatchService( val workdayPolicies = findWorkdayPolicies(date) if (workdayPolicies.isEmpty()) return + val workdayMemberIds = workdayPolicies.map { it.memberId } val alreadyGeneratedMemberIds = notificationLogRepository - .findMemberIdsByScheduledDateAndNotificationType(date, NotificationType.CLOCK_IN) + .findMemberIdsByScheduledDateAndNotificationTypeAndMemberIdIn(date, NotificationType.CLOCK_IN, workdayMemberIds) .toSet() val targetPolicies = workdayPolicies.filter { it.memberId !in alreadyGeneratedMemberIds } diff --git a/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt b/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt index 8dee781..20cde3c 100644 --- a/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt +++ b/src/main/kotlin/com/moa/service/notification/PaydayNotificationBatchService.kt @@ -26,8 +26,9 @@ class PaydayNotificationBatchService( val profiles = findPaydayProfiles(date) if (profiles.isEmpty()) return + val profileMemberIds = profiles.map { it.memberId } val alreadyGeneratedMemberIds = notificationLogRepository - .findMemberIdsByScheduledDateAndNotificationType(date, NotificationType.PAYDAY) + .findMemberIdsByScheduledDateAndNotificationTypeAndMemberIdIn(date, NotificationType.PAYDAY, profileMemberIds) .toSet() val targetProfiles = profiles.filter { it.memberId !in alreadyGeneratedMemberIds }