44import com .kosa .noticeserver .domain .model .event .PaymentEvent ;
55import com .kosa .noticeserver .infrastructure .repository .TokenRepository ;
66import com .kosa .noticeserver .infrastructure .sender .fcm .FCMSender ;
7+ import com .kosa .noticeserver .infrastructure .sender .fcm .FcmFailureClassifier ;
78import lombok .RequiredArgsConstructor ;
89import lombok .extern .slf4j .Slf4j ;
910import org .jboss .logging .MDC ;
@@ -22,6 +23,7 @@ public class PaymentEventNotificationService {
2223 private final FCMSender fcmsender ;
2324 private final NotificationService notificationService ;
2425 private final NotificationDeliveryService notificationDeliveryService ;
26+ private final FcmFailureClassifier fcmFailureClassifier ;
2527
2628 public void notice (PaymentEvent paymentEvent , String eventId ) {
2729
@@ -43,7 +45,9 @@ public void notice(PaymentEvent paymentEvent, String eventId) {
4345 );
4446 if (delivery .isEmpty ()) return ;
4547
46- List <SendNotificationCommand > commands = tokens .stream ().map (token -> buildNotification (token , paymentEvent )).toList ();
48+ List <SendNotificationCommand > commands = tokens .stream ()
49+ .map (token -> buildNotification (token , paymentEvent , eventId ))
50+ .toList ();
4751
4852 SendBatchResult send ;
4953 try {
@@ -54,6 +58,8 @@ public void notice(PaymentEvent paymentEvent, String eventId) {
5458 return ;
5559 }
5660
61+ cleanupInvalidTokens (send .results ());
62+
5763 if (send .successCount () > 0 ) {
5864 notificationDeliveryService .markSent (delivery .get ());
5965 } else {
@@ -113,7 +119,7 @@ public void notice(List<PaymentEvent> paymentEvents, String eventId) {
113119 if (delivery .isEmpty ()) return Stream .empty ();
114120
115121 claimedDeliveries .put (event .getUserId (), delivery .get ());
116- return userTokens .stream ().map (token -> buildNotification (token , event ));
122+ return userTokens .stream ().map (token -> buildNotification (token , event , eventId ));
117123
118124 })
119125 .toList ();
@@ -130,6 +136,7 @@ public void notice(List<PaymentEvent> paymentEvents, String eventId) {
130136 return ;
131137 }
132138
139+ cleanupInvalidTokens (result .results ());
133140 markBulkDeliveryResults (result , claimedDeliveries );
134141 for (SendDetails detail : result .results ()) {
135142 if (detail .isSuccess ()) {
@@ -167,7 +174,30 @@ private String firstErrorMessage(List<SendDetails> details) {
167174 .orElse ("FCM send failed" );
168175 }
169176
170- private SendNotificationCommand buildNotification (String token , PaymentEvent event ) {
177+ private void cleanupInvalidTokens (List <SendDetails > details ) {
178+ List <String > invalidTokens = details .stream ()
179+ .filter (detail -> !detail .isSuccess ())
180+ .filter (fcmFailureClassifier ::isInvalidToken )
181+ .map (SendDetails ::originalCommand )
182+ .filter (Objects ::nonNull )
183+ .map (SendNotificationCommand ::target )
184+ .filter (Objects ::nonNull )
185+ .distinct ()
186+ .toList ();
187+
188+ if (invalidTokens .isEmpty ()) {
189+ return ;
190+ }
191+
192+ try {
193+ long deletedCount = tokenRepository .deleteByTokenIn (invalidTokens );
194+ log .warn ("[{}] invalid FCM tokens deleted, tokenCount: {}" , MDC .get ("eventId" ), deletedCount );
195+ } catch (RuntimeException e ) {
196+ log .error ("[{}] invalid FCM token cleanup failed, tokenCount: {}" , MDC .get ("eventId" ), invalidTokens .size (), e );
197+ }
198+ }
199+
200+ private SendNotificationCommand buildNotification (String token , PaymentEvent event , String eventId ) {
171201 String title = "결제 완료 안내" ;
172202 String body = String .format ("%s님, %s원 결제가 정상 처리되었습니다." ,
173203 event .getUserName (), event .getAmount ());
@@ -176,6 +206,9 @@ private SendNotificationCommand buildNotification(String token, PaymentEvent eve
176206 data .put ("orderId" , event .getOrderId ());
177207 data .put ("paymentTime" , event .getTimestamp ());
178208 data .put ("userId" , event .getUserId ());
209+ data .put ("eventId" , eventId );
210+ data .put ("notificationType" , NotificationType .PAYMENT .name ());
211+ data .put ("dedupKey" , "PAYMENT_COMPLETED:" + event .getOrderId ());
179212
180213 return new SendNotificationCommand (
181214 token ,
0 commit comments