From b5aebffdd4e45eb446a9f0b8c0feb1e910f38ed2 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 19:18:39 +0900 Subject: [PATCH 01/15] =?UTF-8?q?feat:=20PushSubscriptionJpaRepository=20?= =?UTF-8?q?=EB=B0=8F=20PushSubscriptionRepository=EC=97=90=20deleteByToken?= =?UTF-8?q?s=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/push/PushSubscriptionRepository.java | 3 +++ .../persistence/push/PushSubscriptionJpaRepository.java | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/main/java/me/pinitnotification/domain/push/PushSubscriptionRepository.java b/src/main/java/me/pinitnotification/domain/push/PushSubscriptionRepository.java index be393d4..bc688d6 100644 --- a/src/main/java/me/pinitnotification/domain/push/PushSubscriptionRepository.java +++ b/src/main/java/me/pinitnotification/domain/push/PushSubscriptionRepository.java @@ -1,5 +1,6 @@ package me.pinitnotification.domain.push; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -12,5 +13,7 @@ public interface PushSubscriptionRepository { void deleteByToken(String token); + void deleteByTokens(Collection tokens); + void deleteByMemberIdAndDeviceId(Long memberId, String deviceId); } diff --git a/src/main/java/me/pinitnotification/infrastructure/persistence/push/PushSubscriptionJpaRepository.java b/src/main/java/me/pinitnotification/infrastructure/persistence/push/PushSubscriptionJpaRepository.java index 493d40b..811114f 100644 --- a/src/main/java/me/pinitnotification/infrastructure/persistence/push/PushSubscriptionJpaRepository.java +++ b/src/main/java/me/pinitnotification/infrastructure/persistence/push/PushSubscriptionJpaRepository.java @@ -2,6 +2,7 @@ import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -12,5 +13,7 @@ public interface PushSubscriptionJpaRepository extends JpaRepository tokens); + void deleteByMemberIdAndDeviceId(Long memberId, String deviceId); } From 1f491eb964b6b87b6ad0e49509c633ab25c1cd51 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 19:19:59 +0900 Subject: [PATCH 02/15] =?UTF-8?q?feat:=20PushSubscriptionJpaRepository=20?= =?UTF-8?q?=EB=B0=8F=20PushSubscriptionRepository=EC=97=90=20deleteByToken?= =?UTF-8?q?s=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...PushSubscriptionRepositoryAdapterTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/test/java/me/pinitnotification/infrastructure/persistence/push/PushSubscriptionRepositoryAdapterTest.java b/src/test/java/me/pinitnotification/infrastructure/persistence/push/PushSubscriptionRepositoryAdapterTest.java index 57990bc..a30b88c 100644 --- a/src/test/java/me/pinitnotification/infrastructure/persistence/push/PushSubscriptionRepositoryAdapterTest.java +++ b/src/test/java/me/pinitnotification/infrastructure/persistence/push/PushSubscriptionRepositoryAdapterTest.java @@ -17,6 +17,8 @@ class PushSubscriptionRepositoryAdapterTest { @Autowired private PushSubscriptionRepository repository; + @Autowired + private PushSubscriptionJpaRepository jpaRepository; @Test void savesAndLoadsDomainWithPublicId() { @@ -38,4 +40,29 @@ void savesAndLoadsDomainWithPublicId() { assertThat(loaded).isPresent(); assertThat(loaded.get().getId()).isEqualTo(publicId); } + + @Test + void deletesTokensInBatch() { + PushSubscriptionEntity token1 = subscription("token-1", "device-1"); + PushSubscriptionEntity token2 = subscription("token-2", "device-2"); + PushSubscriptionEntity token3 = subscription("token-3", "device-3"); + jpaRepository.save(token1); + jpaRepository.save(token2); + jpaRepository.save(token3); + + repository.deleteByTokens(java.util.List.of("token-1", "token-x")); + + assertThat(jpaRepository.findAll()) + .extracting(PushSubscriptionEntity::getToken) + .containsExactlyInAnyOrder("token-2", "token-3"); + } + + private PushSubscriptionEntity subscription(String token, String deviceId) { + PushSubscriptionEntity entity = new PushSubscriptionEntity(); + entity.setPublicId(UUID.randomUUID()); + entity.setMemberId(201L); + entity.setDeviceId(deviceId); + entity.setToken(token); + return entity; + } } From 900553ba5ba4a36d144d85dc7817ba741a9e40bd Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 19:20:22 +0900 Subject: [PATCH 03/15] =?UTF-8?q?feat:=20PushSubscriptionRepositoryAdapter?= =?UTF-8?q?=EC=97=90=20deleteByTokens=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../push/PushSubscriptionRepositoryAdapter.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/me/pinitnotification/infrastructure/persistence/push/PushSubscriptionRepositoryAdapter.java b/src/main/java/me/pinitnotification/infrastructure/persistence/push/PushSubscriptionRepositoryAdapter.java index 6362285..a1abe25 100644 --- a/src/main/java/me/pinitnotification/infrastructure/persistence/push/PushSubscriptionRepositoryAdapter.java +++ b/src/main/java/me/pinitnotification/infrastructure/persistence/push/PushSubscriptionRepositoryAdapter.java @@ -4,6 +4,7 @@ import me.pinitnotification.domain.push.PushSubscriptionRepository; import org.springframework.stereotype.Repository; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -39,6 +40,14 @@ public void deleteByToken(String token) { jpaRepository.deleteByToken(token); } + @Override + public void deleteByTokens(Collection tokens) { + if (tokens == null || tokens.isEmpty()) { + return; + } + jpaRepository.deleteByTokenIn(tokens); + } + @Override public void deleteByMemberIdAndDeviceId(Long memberId, String deviceId) { jpaRepository.deleteByMemberIdAndDeviceId(memberId, deviceId); From 67ab83a9133590c5a44dbdcf71cc2baab3fc8e28 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 19:20:41 +0900 Subject: [PATCH 04/15] =?UTF-8?q?feat:=20PushSendResult=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EC=B2=98=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/push/PushSendResult.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/me/pinitnotification/application/push/PushSendResult.java diff --git a/src/main/java/me/pinitnotification/application/push/PushSendResult.java b/src/main/java/me/pinitnotification/application/push/PushSendResult.java new file mode 100644 index 0000000..f87d5c2 --- /dev/null +++ b/src/main/java/me/pinitnotification/application/push/PushSendResult.java @@ -0,0 +1,22 @@ +package me.pinitnotification.application.push; + +public record PushSendResult( + boolean success, + boolean invalidToken +) { + public static PushSendResult successResult() { + return new PushSendResult(true, false); + } + + public static PushSendResult invalidTokenResult() { + return new PushSendResult(false, true); + } + + public static PushSendResult failedResult() { + return new PushSendResult(false, false); + } + + public boolean shouldDeleteToken() { + return invalidToken; + } +} From 0c757cf5cb5c270475c1a3be95c5542377121303 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 19:20:47 +0900 Subject: [PATCH 05/15] =?UTF-8?q?feat:=20sendPushMessage=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=EC=9D=98=20=EB=B0=98=ED=99=98=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20PushSendResult=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../me/pinitnotification/application/push/PushService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/me/pinitnotification/application/push/PushService.java b/src/main/java/me/pinitnotification/application/push/PushService.java index 284ea16..e339492 100644 --- a/src/main/java/me/pinitnotification/application/push/PushService.java +++ b/src/main/java/me/pinitnotification/application/push/PushService.java @@ -8,7 +8,8 @@ public interface PushService { void subscribe(Long memberId, String deviceId, String token); void unsubscribe(Long memberId, String deviceId, String token); - void sendPushMessage(String token, Notification notification); + + PushSendResult sendPushMessage(String token, Notification notification); boolean isSubscribed(Long memberId, String deviceId); } From 6aa26f071ddd4ac313e1b76428b654122f070295 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 19:24:21 +0900 Subject: [PATCH 06/15] =?UTF-8?q?feat:=20sendPushMessage=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=EC=9D=98=20=EB=B0=98=ED=99=98=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EC=9D=84=20PushSendResult=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pinitnotification/infrastructure/fcm/FcmService.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/me/pinitnotification/infrastructure/fcm/FcmService.java b/src/main/java/me/pinitnotification/infrastructure/fcm/FcmService.java index d4dc08b..7c04531 100644 --- a/src/main/java/me/pinitnotification/infrastructure/fcm/FcmService.java +++ b/src/main/java/me/pinitnotification/infrastructure/fcm/FcmService.java @@ -5,6 +5,7 @@ import com.google.firebase.messaging.Message; import com.google.firebase.messaging.MessagingErrorCode; import lombok.extern.slf4j.Slf4j; +import me.pinitnotification.application.push.PushSendResult; import me.pinitnotification.application.push.PushService; import me.pinitnotification.domain.notification.Notification; import me.pinitnotification.domain.push.PushSubscription; @@ -34,7 +35,7 @@ public FcmService(FirebaseMessaging firebaseMessaging, } @Override - public void sendPushMessage(String token, Notification notification) { + public PushSendResult sendPushMessage(String token, Notification notification) { log.info("publish token: {}", token); Message message = Message.builder() .setToken(token) @@ -42,12 +43,13 @@ public void sendPushMessage(String token, Notification notification) { .build(); try { firebaseMessaging.send(message); + return PushSendResult.successResult(); } catch (FirebaseMessagingException e) { log.error(e.getMessage(), e); if (e.getMessagingErrorCode() == MessagingErrorCode.UNREGISTERED || e.getMessagingErrorCode() == MessagingErrorCode.INVALID_ARGUMENT) { - // Todo 토큰 삭제 방식 변경 필요 - pushSubscriptionRepository.deleteByToken(token); + return PushSendResult.invalidTokenResult(); } + return PushSendResult.failedResult(); } } From ee0e4b2ebc7c5f131ede0a2d0f73c975f181deeb Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 19:24:36 +0900 Subject: [PATCH 07/15] =?UTF-8?q?feat:=20UpcomingScheduleNotificationEntit?= =?UTF-8?q?y=EC=97=90=20schedule=5Fstart=5Ftime=20=EC=9D=B8=EB=8D=B1?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/UpcomingScheduleNotificationEntity.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationEntity.java b/src/main/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationEntity.java index 6655976..14e402f 100644 --- a/src/main/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationEntity.java +++ b/src/main/java/me/pinitnotification/infrastructure/persistence/notification/UpcomingScheduleNotificationEntity.java @@ -15,6 +15,9 @@ uniqueConstraints = { @UniqueConstraint(name = "uk_schedule_owner", columnNames = {"schedule_id", "owner_id"}), @UniqueConstraint(name = "uk_idempotent_key", columnNames = {"idempotent_key"}) + }, + indexes = { + @Index(name = "idx_upcoming_start_time", columnList = "schedule_start_time") } ) @Getter From ba187da2c9b03e14e2c93b5e87a4b2c555bd02dc Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 19:26:10 +0900 Subject: [PATCH 08/15] =?UTF-8?q?feat:=20NotificationDispatchItem=20?= =?UTF-8?q?=EB=A0=88=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/NotificationDispatchItem.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/me/pinitnotification/application/notification/NotificationDispatchItem.java diff --git a/src/main/java/me/pinitnotification/application/notification/NotificationDispatchItem.java b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchItem.java new file mode 100644 index 0000000..51901f1 --- /dev/null +++ b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchItem.java @@ -0,0 +1,11 @@ +package me.pinitnotification.application.notification; + +import me.pinitnotification.domain.notification.UpcomingScheduleNotification; + +import java.util.List; + +public record NotificationDispatchItem( + UpcomingScheduleNotification notification, + List tokens +) { +} From 74d499ed6ea345b5f9370fe021a35964a23f2748 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 19:26:25 +0900 Subject: [PATCH 09/15] =?UTF-8?q?feat:=20NotificationDispatchQueryReposito?= =?UTF-8?q?ry=20=EC=9D=B8=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/NotificationDispatchQueryRepository.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/me/pinitnotification/application/notification/NotificationDispatchQueryRepository.java diff --git a/src/main/java/me/pinitnotification/application/notification/NotificationDispatchQueryRepository.java b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchQueryRepository.java new file mode 100644 index 0000000..6aebd58 --- /dev/null +++ b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchQueryRepository.java @@ -0,0 +1,8 @@ +package me.pinitnotification.application.notification; + +import java.time.Instant; +import java.util.List; + +public interface NotificationDispatchQueryRepository { + List findDueNotificationsWithTokens(Instant now); +} From 928a6682a0cea1c6ced3b706491f8e4fc63d1ce7 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 19:26:37 +0900 Subject: [PATCH 10/15] =?UTF-8?q?feat:=20NotificationDispatchQueryReposito?= =?UTF-8?q?ryAdapter=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=95=8C=EB=A6=BC?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=EC=A7=91=EA=B3=84=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...icationDispatchQueryRepositoryAdapter.java | 103 ++++++++++++++++++ ...ionDispatchQueryRepositoryAdapterTest.java | 82 ++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 src/main/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapter.java create mode 100644 src/test/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapterTest.java diff --git a/src/main/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapter.java b/src/main/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapter.java new file mode 100644 index 0000000..231ec91 --- /dev/null +++ b/src/main/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapter.java @@ -0,0 +1,103 @@ +package me.pinitnotification.infrastructure.persistence.notification; + +import me.pinitnotification.application.notification.NotificationDispatchItem; +import me.pinitnotification.application.notification.NotificationDispatchQueryRepository; +import me.pinitnotification.domain.notification.UpcomingScheduleNotification; +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.stereotype.Repository; + +import java.time.Instant; +import java.util.*; + +@Repository +public class NotificationDispatchQueryRepositoryAdapter implements NotificationDispatchQueryRepository { + private static final String FIND_DUE_WITH_TOKENS_SQL = """ + SELECT n.public_id, + n.owner_id, + n.schedule_id, + n.schedule_title, + n.schedule_start_time, + n.idempotent_key, + ps.token + FROM upcoming_schedule_notification n + LEFT JOIN push_subscription ps ON ps.member_id = n.owner_id + WHERE n.schedule_start_time IS NOT NULL + AND n.schedule_start_time <= ? + """; + + private final JdbcClient jdbcClient; + + public NotificationDispatchQueryRepositoryAdapter(JdbcClient jdbcClient) { + this.jdbcClient = jdbcClient; + } + + @Override + public List findDueNotificationsWithTokens(Instant now) { + List rows = jdbcClient.sql(FIND_DUE_WITH_TOKENS_SQL) + .param(now.toString()) + .query((rs, rowNum) -> new DispatchRow( + UUID.fromString(rs.getString("public_id")), + rs.getLong("owner_id"), + rs.getLong("schedule_id"), + rs.getString("schedule_title"), + rs.getString("schedule_start_time"), + rs.getString("idempotent_key"), + rs.getString("token") + )) + .list(); + + if (rows.isEmpty()) { + return List.of(); + } + + Map aggregated = new LinkedHashMap<>(); + for (DispatchRow row : rows) { + DispatchAccumulator accumulator = aggregated.computeIfAbsent( + row.notificationId, + id -> new DispatchAccumulator( + toDomain(row), + new ArrayList<>() + ) + ); + if (row.token != null) { + accumulator.tokens.add(row.token); + } + } + + return aggregated.values().stream() + .map(accumulator -> new NotificationDispatchItem(accumulator.notification, List.copyOf(accumulator.tokens))) + .toList(); + } + + private UpcomingScheduleNotification toDomain(DispatchRow row) { + return new UpcomingScheduleNotification( + row.notificationId, + row.ownerId, + row.scheduleId, + row.scheduleTitle, + row.scheduleStartTime, + row.idempotentKey + ); + } + + private record DispatchRow( + UUID notificationId, + Long ownerId, + Long scheduleId, + String scheduleTitle, + String scheduleStartTime, + String idempotentKey, + String token + ) { + } + + private static class DispatchAccumulator { + private final UpcomingScheduleNotification notification; + private final List tokens; + + private DispatchAccumulator(UpcomingScheduleNotification notification, List tokens) { + this.notification = notification; + this.tokens = tokens; + } + } +} diff --git a/src/test/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapterTest.java b/src/test/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapterTest.java new file mode 100644 index 0000000..b53c7ae --- /dev/null +++ b/src/test/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapterTest.java @@ -0,0 +1,82 @@ +package me.pinitnotification.infrastructure.persistence.notification; + +import jakarta.persistence.EntityManager; +import me.pinitnotification.application.notification.NotificationDispatchItem; +import me.pinitnotification.application.notification.NotificationDispatchQueryRepository; +import me.pinitnotification.domain.push.PushSubscription; +import me.pinitnotification.domain.push.PushSubscriptionRepository; +import me.pinitnotification.infrastructure.persistence.push.PushSubscriptionRepositoryAdapter; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@Import({NotificationDispatchQueryRepositoryAdapter.class, PushSubscriptionRepositoryAdapter.class}) +class NotificationDispatchQueryRepositoryAdapterTest { + + @Autowired + private NotificationDispatchQueryRepository repository; + @Autowired + private UpcomingScheduleNotificationJpaRepository notificationJpaRepository; + @Autowired + private PushSubscriptionRepository pushSubscriptionRepository; + @Autowired + private EntityManager entityManager; + + @Test + void returnsDueNotificationsWithAggregatedTokens() { + // given + UpcomingScheduleNotificationEntity dueWithTokens = notificationJpaRepository.save(notification(1L, 11L, "2024-06-01T09:50:00Z")); + notificationJpaRepository.save(notification(2L, 12L, "2024-06-01T09:55:00Z")); // no tokens + notificationJpaRepository.save(notification(3L, 13L, "2024-06-01T10:30:00Z")); // future, should be excluded + + pushSubscriptionRepository.save(subscription(1L, "device-1", "token-1")); + pushSubscriptionRepository.save(subscription(1L, "device-2", "token-2")); + + entityManager.flush(); + entityManager.clear(); + + // when + List results = repository.findDueNotificationsWithTokens(Instant.parse("2024-06-01T10:00:00Z")); + + // then + assertThat(results) + .hasSize(2) + .extracting(item -> item.notification().getScheduleId()) + .containsExactlyInAnyOrder(11L, 12L); + + NotificationDispatchItem withTokens = results.stream() + .filter(item -> item.notification().getScheduleId().equals(11L)) + .findFirst() + .orElseThrow(); + assertThat(withTokens.tokens()).containsExactlyInAnyOrder("token-1", "token-2"); + + NotificationDispatchItem withoutTokens = results.stream() + .filter(item -> item.notification().getScheduleId().equals(12L)) + .findFirst() + .orElseThrow(); + assertThat(withoutTokens.tokens()).isEmpty(); + } + + private UpcomingScheduleNotificationEntity notification(Long ownerId, Long scheduleId, String startTimeIso) { + UpcomingScheduleNotificationEntity entity = new UpcomingScheduleNotificationEntity(); + entity.setPublicId(UUID.randomUUID()); + entity.setOwnerId(ownerId); + entity.setScheduleId(scheduleId); + entity.setScheduleTitle("title-" + scheduleId); + entity.setScheduleStartTime(startTimeIso); + entity.setIdempotentKey("key-" + scheduleId); + return entity; + } + + private PushSubscription subscription(Long memberId, String deviceId, String token) { + return new PushSubscription(UUID.randomUUID(), memberId, deviceId, token); + } +} From d345d281a73d8877f92ce9cc7d733d6895872f3e Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 19:27:05 +0900 Subject: [PATCH 11/15] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=EB=B0=B0?= =?UTF-8?q?=EC=B9=98=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=20=ED=91=B8=EC=8B=9C=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=20=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationDispatchScheduler.java | 44 +++++++++++------ .../NotificationDispatchSchedulerTest.java | 47 +++++++++++++------ 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java index 7e0daa6..b72915e 100644 --- a/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java +++ b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java @@ -1,10 +1,10 @@ package me.pinitnotification.application.notification; +import me.pinitnotification.application.push.PushSendResult; +import me.pinitnotification.application.push.PushService; import me.pinitnotification.domain.notification.UpcomingScheduleNotification; import me.pinitnotification.domain.notification.UpcomingScheduleNotificationRepository; -import me.pinitnotification.domain.push.PushSubscription; import me.pinitnotification.domain.push.PushSubscriptionRepository; -import me.pinitnotification.application.push.PushService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; @@ -12,24 +12,28 @@ import org.springframework.transaction.annotation.Transactional; import java.time.Clock; -import java.time.OffsetDateTime; -import java.time.format.DateTimeParseException; +import java.time.Instant; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; @Service public class NotificationDispatchScheduler { private static final Logger log = LoggerFactory.getLogger(NotificationDispatchScheduler.class); private final UpcomingScheduleNotificationRepository notificationRepository; + private final NotificationDispatchQueryRepository dispatchQueryRepository; private final PushSubscriptionRepository pushSubscriptionRepository; private final PushService pushService; private final Clock clock; public NotificationDispatchScheduler(UpcomingScheduleNotificationRepository notificationRepository, + NotificationDispatchQueryRepository dispatchQueryRepository, PushSubscriptionRepository pushSubscriptionRepository, PushService pushService, Clock clock) { this.notificationRepository = notificationRepository; + this.dispatchQueryRepository = dispatchQueryRepository; this.pushSubscriptionRepository = pushSubscriptionRepository; this.pushService = pushService; this.clock = clock; @@ -38,27 +42,37 @@ public NotificationDispatchScheduler(UpcomingScheduleNotificationRepository noti @Scheduled(cron = "0 */10 * * * *") @Transactional public void dispatchDueNotifications() { - OffsetDateTime now = OffsetDateTime.now(clock); - List dueNotifications = notificationRepository.findAll().stream() - .filter(notification -> notification.isDue(now)) - .toList(); + Instant now = Instant.now(clock); + List dispatchItems = dispatchQueryRepository.findDueNotificationsWithTokens(now); - if (dueNotifications.isEmpty()) { + if (dispatchItems.isEmpty()) { return; } - dueNotifications.forEach(this::sendNotificationToOwner); - notificationRepository.deleteAllInBatch(dueNotifications); + Set tokensToDelete = new LinkedHashSet<>(); + dispatchItems.forEach(item -> sendNotificationToOwner(item, tokensToDelete)); + notificationRepository.deleteAllInBatch(dispatchItems.stream().map(NotificationDispatchItem::notification).toList()); + + if (!tokensToDelete.isEmpty()) { + pushSubscriptionRepository.deleteByTokens(tokensToDelete); + } } - private void sendNotificationToOwner(UpcomingScheduleNotification notification) { - List subscriptions = pushSubscriptionRepository.findAllByMemberId(notification.getOwnerId()); - if (subscriptions.isEmpty()) { + private void sendNotificationToOwner(NotificationDispatchItem dispatchItem, Set tokensToDelete) { + UpcomingScheduleNotification notification = dispatchItem.notification(); + List tokens = dispatchItem.tokens(); + + if (tokens.isEmpty()) { log.info("No push tokens for owner; skip sending. ownerId={}, scheduleId={}", notification.getOwnerId(), notification.getScheduleId()); return; } - subscriptions.forEach(subscription -> pushService.sendPushMessage(subscription.getToken(), notification)); + tokens.forEach(token -> { + PushSendResult result = pushService.sendPushMessage(token, notification); + if (result.shouldDeleteToken()) { + tokensToDelete.add(token); + } + }); } } diff --git a/src/test/java/me/pinitnotification/application/notification/NotificationDispatchSchedulerTest.java b/src/test/java/me/pinitnotification/application/notification/NotificationDispatchSchedulerTest.java index 278ae35..54195ab 100644 --- a/src/test/java/me/pinitnotification/application/notification/NotificationDispatchSchedulerTest.java +++ b/src/test/java/me/pinitnotification/application/notification/NotificationDispatchSchedulerTest.java @@ -1,9 +1,9 @@ package me.pinitnotification.application.notification; +import me.pinitnotification.application.push.PushSendResult; import me.pinitnotification.application.push.PushService; import me.pinitnotification.domain.notification.UpcomingScheduleNotification; import me.pinitnotification.domain.notification.UpcomingScheduleNotificationRepository; -import me.pinitnotification.domain.push.PushSubscription; import me.pinitnotification.domain.push.PushSubscriptionRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -15,6 +15,7 @@ import java.time.Instant; import java.time.ZoneOffset; import java.util.List; +import java.util.Set; import static org.mockito.Mockito.*; @@ -24,6 +25,8 @@ class NotificationDispatchSchedulerTest { @Mock private UpcomingScheduleNotificationRepository notificationRepository; @Mock + private NotificationDispatchQueryRepository dispatchQueryRepository; + @Mock private PushSubscriptionRepository pushSubscriptionRepository; @Mock private PushService pushService; @@ -34,36 +37,52 @@ class NotificationDispatchSchedulerTest { @BeforeEach void setUp() { clock = Clock.fixed(Instant.parse("2024-06-01T10:00:00Z"), ZoneOffset.UTC); - scheduler = new NotificationDispatchScheduler(notificationRepository, pushSubscriptionRepository, pushService, clock); + scheduler = new NotificationDispatchScheduler(notificationRepository, dispatchQueryRepository, pushSubscriptionRepository, pushService, clock); } @Test - void dispatchDueNotifications_sendsAndDeletesPastNotifications() { - UpcomingScheduleNotification past = new UpcomingScheduleNotification(1L, 10L, "title", "2024-06-01T09:50Z", "key-1"); - UpcomingScheduleNotification future = new UpcomingScheduleNotification(1L, 11L, "title2", "2024-06-01T10:30Z", "key-2"); + void dispatchDueNotifications_sendsAndDeletesNotificationsFromQuery() { + UpcomingScheduleNotification notification = new UpcomingScheduleNotification(1L, 10L, "title", "2024-06-01T09:50Z", "key-1"); - when(notificationRepository.findAll()).thenReturn(List.of(past, future)); - when(pushSubscriptionRepository.findAllByMemberId(1L)) - .thenReturn(List.of(new PushSubscription(1L, "device-1", "token-1"), new PushSubscription(1L, "device-2", "token-2"))); + when(dispatchQueryRepository.findDueNotificationsWithTokens(any())) + .thenReturn(List.of(new NotificationDispatchItem(notification, List.of("token-1", "token-2")))); + when(pushService.sendPushMessage(anyString(), eq(notification))).thenReturn(PushSendResult.successResult()); scheduler.dispatchDueNotifications(); - verify(pushService).sendPushMessage("token-1", past); - verify(pushService).sendPushMessage("token-2", past); - verify(notificationRepository).deleteAllInBatch(List.of(past)); - verify(pushService, never()).sendPushMessage(anyString(), eq(future)); + verify(pushService).sendPushMessage("token-1", notification); + verify(pushService).sendPushMessage("token-2", notification); + verify(notificationRepository).deleteAllInBatch(List.of(notification)); + verify(pushSubscriptionRepository, never()).deleteByTokens(any()); } @Test void dispatchDueNotifications_deletesEvenWhenNoTokens() { UpcomingScheduleNotification past = new UpcomingScheduleNotification(2L, 20L, "title", "2024-06-01T09:00Z", "key-3"); - when(notificationRepository.findAll()).thenReturn(List.of(past)); - when(pushSubscriptionRepository.findAllByMemberId(2L)).thenReturn(List.of()); + when(dispatchQueryRepository.findDueNotificationsWithTokens(any())) + .thenReturn(List.of(new NotificationDispatchItem(past, List.of()))); scheduler.dispatchDueNotifications(); verify(pushService, never()).sendPushMessage(anyString(), any()); verify(notificationRepository).deleteAllInBatch(List.of(past)); + verify(pushSubscriptionRepository, never()).deleteByTokens(any()); + } + + @Test + void dispatchDueNotifications_collectsInvalidTokensAndDeletesInBatch() { + UpcomingScheduleNotification notification = new UpcomingScheduleNotification(3L, 30L, "title", "2024-06-01T09:30Z", "key-4"); + + when(dispatchQueryRepository.findDueNotificationsWithTokens(any())) + .thenReturn(List.of(new NotificationDispatchItem(notification, List.of("token-1", "token-2", "token-3")))); + when(pushService.sendPushMessage("token-1", notification)).thenReturn(PushSendResult.invalidTokenResult()); + when(pushService.sendPushMessage("token-2", notification)).thenReturn(PushSendResult.failedResult()); + when(pushService.sendPushMessage("token-3", notification)).thenReturn(PushSendResult.successResult()); + + scheduler.dispatchDueNotifications(); + + verify(pushSubscriptionRepository).deleteByTokens(Set.of("token-1")); + verify(notificationRepository).deleteAllInBatch(List.of(notification)); } } From a2a7e58b4117993463c76d545bfbf97e7b9e4afc Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 19:59:50 +0900 Subject: [PATCH 12/15] =?UTF-8?q?feat:=20NotificationDispatchScheduler?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=86=A0=ED=81=B0=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EC=A7=91=ED=95=A9=EC=9D=84=20HashSet=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/NotificationDispatchScheduler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java index b72915e..e52e0e5 100644 --- a/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java +++ b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java @@ -13,7 +13,7 @@ import java.time.Clock; import java.time.Instant; -import java.util.LinkedHashSet; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -49,7 +49,7 @@ public void dispatchDueNotifications() { return; } - Set tokensToDelete = new LinkedHashSet<>(); + Set tokensToDelete = new HashSet<>(); dispatchItems.forEach(item -> sendNotificationToOwner(item, tokensToDelete)); notificationRepository.deleteAllInBatch(dispatchItems.stream().map(NotificationDispatchItem::notification).toList()); From 6a9e4ec6291cdb94e682d0023652f423f798f832 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 20:05:42 +0900 Subject: [PATCH 13/15] =?UTF-8?q?feat:=20PushTokenCleanupService=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=ED=91=B8=EC=8B=9C=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/PushTokenCleanupService.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/main/java/me/pinitnotification/application/notification/PushTokenCleanupService.java diff --git a/src/main/java/me/pinitnotification/application/notification/PushTokenCleanupService.java b/src/main/java/me/pinitnotification/application/notification/PushTokenCleanupService.java new file mode 100644 index 0000000..46e53dd --- /dev/null +++ b/src/main/java/me/pinitnotification/application/notification/PushTokenCleanupService.java @@ -0,0 +1,29 @@ +package me.pinitnotification.application.notification; + +import me.pinitnotification.domain.push.PushSubscriptionRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Set; + +@Service +public class PushTokenCleanupService { + private static final Logger log = LoggerFactory.getLogger(PushTokenCleanupService.class); + private final PushSubscriptionRepository pushSubscriptionRepository; + + public PushTokenCleanupService(PushSubscriptionRepository pushSubscriptionRepository) { + this.pushSubscriptionRepository = pushSubscriptionRepository; + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void deleteTokensInNewTransaction(Set tokens) { + if (tokens == null || tokens.isEmpty()) { + return; + } + pushSubscriptionRepository.deleteByTokens(tokens); + log.info("Deleted {} invalid push tokens", tokens.size()); + } +} From 09b689777685373f6423529910a116f29a21c184 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 20:05:48 +0900 Subject: [PATCH 14/15] =?UTF-8?q?feat:=20PushTokenCleanupService=EB=A5=BC?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20=ED=91=B8=EC=8B=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NotificationDispatchScheduler.java | 17 ++++++++++++----- .../NotificationDispatchSchedulerTest.java | 11 +++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java index e52e0e5..3cf6e96 100644 --- a/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java +++ b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java @@ -4,7 +4,6 @@ import me.pinitnotification.application.push.PushService; import me.pinitnotification.domain.notification.UpcomingScheduleNotification; import me.pinitnotification.domain.notification.UpcomingScheduleNotificationRepository; -import me.pinitnotification.domain.push.PushSubscriptionRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; @@ -23,18 +22,18 @@ public class NotificationDispatchScheduler { private final UpcomingScheduleNotificationRepository notificationRepository; private final NotificationDispatchQueryRepository dispatchQueryRepository; - private final PushSubscriptionRepository pushSubscriptionRepository; + private final PushTokenCleanupService pushTokenCleanupService; private final PushService pushService; private final Clock clock; public NotificationDispatchScheduler(UpcomingScheduleNotificationRepository notificationRepository, NotificationDispatchQueryRepository dispatchQueryRepository, - PushSubscriptionRepository pushSubscriptionRepository, + PushTokenCleanupService pushTokenCleanupService, PushService pushService, Clock clock) { this.notificationRepository = notificationRepository; this.dispatchQueryRepository = dispatchQueryRepository; - this.pushSubscriptionRepository = pushSubscriptionRepository; + this.pushTokenCleanupService = pushTokenCleanupService; this.pushService = pushService; this.clock = clock; } @@ -54,7 +53,7 @@ public void dispatchDueNotifications() { notificationRepository.deleteAllInBatch(dispatchItems.stream().map(NotificationDispatchItem::notification).toList()); if (!tokensToDelete.isEmpty()) { - pushSubscriptionRepository.deleteByTokens(tokensToDelete); + deleteTokensSafely(tokensToDelete); } } @@ -75,4 +74,12 @@ private void sendNotificationToOwner(NotificationDispatchItem dispatchItem, Set< } }); } + + private void deleteTokensSafely(Set tokensToDelete) { + try { + pushTokenCleanupService.deleteTokensInNewTransaction(tokensToDelete); + } catch (Exception ex) { + log.warn("Failed to delete invalid push tokens; notifications already removed. tokens={}", tokensToDelete.size(), ex); + } + } } diff --git a/src/test/java/me/pinitnotification/application/notification/NotificationDispatchSchedulerTest.java b/src/test/java/me/pinitnotification/application/notification/NotificationDispatchSchedulerTest.java index 54195ab..f95c809 100644 --- a/src/test/java/me/pinitnotification/application/notification/NotificationDispatchSchedulerTest.java +++ b/src/test/java/me/pinitnotification/application/notification/NotificationDispatchSchedulerTest.java @@ -4,7 +4,6 @@ import me.pinitnotification.application.push.PushService; import me.pinitnotification.domain.notification.UpcomingScheduleNotification; import me.pinitnotification.domain.notification.UpcomingScheduleNotificationRepository; -import me.pinitnotification.domain.push.PushSubscriptionRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -27,7 +26,7 @@ class NotificationDispatchSchedulerTest { @Mock private NotificationDispatchQueryRepository dispatchQueryRepository; @Mock - private PushSubscriptionRepository pushSubscriptionRepository; + private PushTokenCleanupService pushTokenCleanupService; @Mock private PushService pushService; @@ -37,7 +36,7 @@ class NotificationDispatchSchedulerTest { @BeforeEach void setUp() { clock = Clock.fixed(Instant.parse("2024-06-01T10:00:00Z"), ZoneOffset.UTC); - scheduler = new NotificationDispatchScheduler(notificationRepository, dispatchQueryRepository, pushSubscriptionRepository, pushService, clock); + scheduler = new NotificationDispatchScheduler(notificationRepository, dispatchQueryRepository, pushTokenCleanupService, pushService, clock); } @Test @@ -53,7 +52,7 @@ void dispatchDueNotifications_sendsAndDeletesNotificationsFromQuery() { verify(pushService).sendPushMessage("token-1", notification); verify(pushService).sendPushMessage("token-2", notification); verify(notificationRepository).deleteAllInBatch(List.of(notification)); - verify(pushSubscriptionRepository, never()).deleteByTokens(any()); + verify(pushTokenCleanupService, never()).deleteTokensInNewTransaction(any()); } @Test @@ -67,7 +66,7 @@ void dispatchDueNotifications_deletesEvenWhenNoTokens() { verify(pushService, never()).sendPushMessage(anyString(), any()); verify(notificationRepository).deleteAllInBatch(List.of(past)); - verify(pushSubscriptionRepository, never()).deleteByTokens(any()); + verify(pushTokenCleanupService, never()).deleteTokensInNewTransaction(any()); } @Test @@ -82,7 +81,7 @@ void dispatchDueNotifications_collectsInvalidTokensAndDeletesInBatch() { scheduler.dispatchDueNotifications(); - verify(pushSubscriptionRepository).deleteByTokens(Set.of("token-1")); + verify(pushTokenCleanupService).deleteTokensInNewTransaction(Set.of("token-1")); verify(notificationRepository).deleteAllInBatch(List.of(notification)); } } From 8f93b53c59f6b394ac29ed6e62dc05a2bc40f231 Mon Sep 17 00:00:00 2001 From: GoGradually Date: Mon, 19 Jan 2026 20:14:47 +0900 Subject: [PATCH 15/15] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=A1=B0=ED=9A=8C=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=9D=BC?= =?UTF-8?q?=EA=B4=80=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../notification/NotificationDispatchQueryRepository.java | 2 +- .../notification/NotificationDispatchScheduler.java | 2 +- .../NotificationDispatchQueryRepositoryAdapter.java | 2 +- .../notification/NotificationDispatchSchedulerTest.java | 6 +++--- .../NotificationDispatchQueryRepositoryAdapterTest.java | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/me/pinitnotification/application/notification/NotificationDispatchQueryRepository.java b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchQueryRepository.java index 6aebd58..2749de9 100644 --- a/src/main/java/me/pinitnotification/application/notification/NotificationDispatchQueryRepository.java +++ b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchQueryRepository.java @@ -4,5 +4,5 @@ import java.util.List; public interface NotificationDispatchQueryRepository { - List findDueNotificationsWithTokens(Instant now); + List findAllDueNotificationsWithTokens(Instant now); } diff --git a/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java index 3cf6e96..ad49d94 100644 --- a/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java +++ b/src/main/java/me/pinitnotification/application/notification/NotificationDispatchScheduler.java @@ -42,7 +42,7 @@ public NotificationDispatchScheduler(UpcomingScheduleNotificationRepository noti @Transactional public void dispatchDueNotifications() { Instant now = Instant.now(clock); - List dispatchItems = dispatchQueryRepository.findDueNotificationsWithTokens(now); + List dispatchItems = dispatchQueryRepository.findAllDueNotificationsWithTokens(now); if (dispatchItems.isEmpty()) { return; diff --git a/src/main/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapter.java b/src/main/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapter.java index 231ec91..d57ccb2 100644 --- a/src/main/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapter.java +++ b/src/main/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapter.java @@ -32,7 +32,7 @@ public NotificationDispatchQueryRepositoryAdapter(JdbcClient jdbcClient) { } @Override - public List findDueNotificationsWithTokens(Instant now) { + public List findAllDueNotificationsWithTokens(Instant now) { List rows = jdbcClient.sql(FIND_DUE_WITH_TOKENS_SQL) .param(now.toString()) .query((rs, rowNum) -> new DispatchRow( diff --git a/src/test/java/me/pinitnotification/application/notification/NotificationDispatchSchedulerTest.java b/src/test/java/me/pinitnotification/application/notification/NotificationDispatchSchedulerTest.java index f95c809..4248b8a 100644 --- a/src/test/java/me/pinitnotification/application/notification/NotificationDispatchSchedulerTest.java +++ b/src/test/java/me/pinitnotification/application/notification/NotificationDispatchSchedulerTest.java @@ -43,7 +43,7 @@ void setUp() { void dispatchDueNotifications_sendsAndDeletesNotificationsFromQuery() { UpcomingScheduleNotification notification = new UpcomingScheduleNotification(1L, 10L, "title", "2024-06-01T09:50Z", "key-1"); - when(dispatchQueryRepository.findDueNotificationsWithTokens(any())) + when(dispatchQueryRepository.findAllDueNotificationsWithTokens(any())) .thenReturn(List.of(new NotificationDispatchItem(notification, List.of("token-1", "token-2")))); when(pushService.sendPushMessage(anyString(), eq(notification))).thenReturn(PushSendResult.successResult()); @@ -59,7 +59,7 @@ void dispatchDueNotifications_sendsAndDeletesNotificationsFromQuery() { void dispatchDueNotifications_deletesEvenWhenNoTokens() { UpcomingScheduleNotification past = new UpcomingScheduleNotification(2L, 20L, "title", "2024-06-01T09:00Z", "key-3"); - when(dispatchQueryRepository.findDueNotificationsWithTokens(any())) + when(dispatchQueryRepository.findAllDueNotificationsWithTokens(any())) .thenReturn(List.of(new NotificationDispatchItem(past, List.of()))); scheduler.dispatchDueNotifications(); @@ -73,7 +73,7 @@ void dispatchDueNotifications_deletesEvenWhenNoTokens() { void dispatchDueNotifications_collectsInvalidTokensAndDeletesInBatch() { UpcomingScheduleNotification notification = new UpcomingScheduleNotification(3L, 30L, "title", "2024-06-01T09:30Z", "key-4"); - when(dispatchQueryRepository.findDueNotificationsWithTokens(any())) + when(dispatchQueryRepository.findAllDueNotificationsWithTokens(any())) .thenReturn(List.of(new NotificationDispatchItem(notification, List.of("token-1", "token-2", "token-3")))); when(pushService.sendPushMessage("token-1", notification)).thenReturn(PushSendResult.invalidTokenResult()); when(pushService.sendPushMessage("token-2", notification)).thenReturn(PushSendResult.failedResult()); diff --git a/src/test/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapterTest.java b/src/test/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapterTest.java index b53c7ae..d1f75f0 100644 --- a/src/test/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapterTest.java +++ b/src/test/java/me/pinitnotification/infrastructure/persistence/notification/NotificationDispatchQueryRepositoryAdapterTest.java @@ -44,7 +44,7 @@ void returnsDueNotificationsWithAggregatedTokens() { entityManager.clear(); // when - List results = repository.findDueNotificationsWithTokens(Instant.parse("2024-06-01T10:00:00Z")); + List results = repository.findAllDueNotificationsWithTokens(Instant.parse("2024-06-01T10:00:00Z")); // then assertThat(results)