NotificationService.java 15.4 KB
package com.ecommerce.notification.service;

import com.ecommerce.notification.model.Notification;
import com.ecommerce.notification.model.dto.NotificationRequest;
import com.ecommerce.notification.model.dto.NotificationResponse;
import com.ecommerce.notification.repository.NotificationRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class NotificationService {
    
    private final NotificationRepository notificationRepository;
    private final EmailService emailService;
    private final SmsService smsService;
    private final PushNotificationService pushNotificationService;
    private final TemplateService templateService;
    private final RabbitMQService rabbitMQService;
    
    @Transactional
    @Caching(evict = {
        @CacheEvict(value = "notifications", allEntries = true),
        @CacheEvict(value = "notification", key = "#result.notificationId")
    })
    public NotificationResponse sendNotification(NotificationRequest request) {
        // Validate notification request
        validateNotificationRequest(request);
        
        // Create notification record
        Notification notification = new Notification();
        mapNotificationRequestToEntity(request, notification);
        
        // Process template if template name is provided
        if (request.getTemplateName() != null) {
            processTemplate(notification, request.getVariables());
        }
        
        Notification savedNotification = notificationRepository.save(notification);
        
        // Send notification based on type
        boolean sent = sendNotificationByType(savedNotification);
        
        // Update notification status
        if (sent) {
            savedNotification.setStatus("SENT");
            savedNotification.setSentAt(LocalDateTime.now());
        } else {
            savedNotification.setStatus("FAILED");
            savedNotification.setRetryCount(savedNotification.getRetryCount() + 1);
        }
        
        Notification updatedNotification = notificationRepository.save(savedNotification);
        
        log.info("Notification {}: {}", sent ? "sent" : "failed", updatedNotification.getNotificationId());
        return mapNotificationToResponse(updatedNotification);
    }
    
    @Cacheable(value = "notification", key = "#notificationId")
    public NotificationResponse getNotification(String notificationId) {
        Notification notification = notificationRepository.findByNotificationId(notificationId)
                .orElseThrow(() -> new RuntimeException("Notification not found: " + notificationId));
        return mapNotificationToResponse(notification);
    }
    
    @Cacheable(value = "notifications", key = "#userId + '-' + #pageable.pageNumber")
    public Page<NotificationResponse> getNotificationsByUser(Long userId, Pageable pageable) {
        return notificationRepository.findByUserId(userId, pageable)
                .map(this::mapNotificationToResponse);
    }
    
    @Cacheable(value = "notifications", key = "#email + '-' + #pageable.pageNumber")
    public Page<NotificationResponse> getNotificationsByEmail(String email, Pageable pageable) {
        return notificationRepository.findByEmail(email, pageable)
                .map(this::mapNotificationToResponse);
    }
    
    @Cacheable(value = "notifications", key = "#type + '-' + #pageable.pageNumber")
    public Page<NotificationResponse> getNotificationsByType(String type, Pageable pageable) {
        return notificationRepository.findByType(type, pageable)
                .map(this::mapNotificationToResponse);
    }
    
    @Cacheable(value = "notifications", key = "#channel + '-' + #pageable.pageNumber")
    public Page<NotificationResponse> getNotificationsByChannel(String channel, Pageable pageable) {
        return notificationRepository.findByChannel(channel, pageable)
                .map(this::mapNotificationToResponse);
    }
    
    @Cacheable(value = "notifications", key = "#pageable.pageNumber + '-' + #pageable.pageSize")
    public Page<NotificationResponse> getAllNotifications(Pageable pageable) {
        return notificationRepository.findAll(pageable)
                .map(this::mapNotificationToResponse);
    }
    
    @Transactional
    @Caching(evict = {
        @CacheEvict(value = "notifications", allEntries = true),
        @CacheEvict(value = "notification", key = "#notificationId")
    })
    public NotificationResponse markAsRead(String notificationId) {
        Notification notification = notificationRepository.findByNotificationId(notificationId)
                .orElseThrow(() -> new RuntimeException("Notification not found: " + notificationId));
        
        notification.setReadAt(LocalDateTime.now());
        Notification updatedNotification = notificationRepository.save(notification);
        
        return mapNotificationToResponse(updatedNotification);
    }
    
    @Transactional
    @Caching(evict = {
        @CacheEvict(value = "notifications", allEntries = true),
        @CacheEvict(value = "notification", key = "#notificationId")
    })
    public NotificationResponse retryNotification(String notificationId) {
        Notification notification = notificationRepository.findByNotificationId(notificationId)
                .orElseThrow(() -> new RuntimeException("Notification not found: " + notificationId));
        
        if (!"FAILED".equals(notification.getStatus())) {
            throw new RuntimeException("Only failed notifications can be retried");
        }
        
        if (notification.getRetryCount() >= notification.getMaxRetries()) {
            throw new RuntimeException("Maximum retry attempts reached");
        }
        
        boolean sent = sendNotificationByType(notification);
        
        if (sent) {
            notification.setStatus("SENT");
            notification.setSentAt(LocalDateTime.now());
        } else {
            notification.setRetryCount(notification.getRetryCount() + 1);
        }
        
        Notification updatedNotification = notificationRepository.save(notification);
        
        log.info("Notification retry {}: {}", sent ? "succeeded" : "failed", notificationId);
        return mapNotificationToResponse(updatedNotification);
    }
    
    @Transactional
    public void processPendingNotifications() {
        List<Notification> pendingNotifications = notificationRepository.findPendingNotifications();
        
        for (Notification notification : pendingNotifications) {
            try {
                boolean sent = sendNotificationByType(notification);
                
                if (sent) {
                    notification.setStatus("SENT");
                    notification.setSentAt(LocalDateTime.now());
                } else {
                    notification.setRetryCount(notification.getRetryCount() + 1);
                    if (notification.getRetryCount() >= notification.getMaxRetries()) {
                        notification.setStatus("FAILED");
                        notification.setFailureReason("Max retry attempts reached");
                    }
                }
                
                notificationRepository.save(notification);
                log.info("Processed pending notification: {} -> {}", 
                        notification.getNotificationId(), notification.getStatus());
                
            } catch (Exception e) {
                log.error("Failed to process pending notification: {}", notification.getNotificationId(), e);
            }
        }
    }
    
    public Map<String, Object> getNotificationStatistics(LocalDateTime startDate, LocalDateTime endDate) {
        Long pendingCount = notificationRepository.countByStatus("PENDING");
        Long sentCount = notificationRepository.countByStatus("SENT");
        Long failedCount = notificationRepository.countByStatus("FAILED");
        
        List<Object[]> channelStats = notificationRepository.getChannelStats(startDate, endDate);
        Map<String, Long> channelCounts = new HashMap<>();
        for (Object[] stat : channelStats) {
            channelCounts.put((String) stat[0], (Long) stat[1]);
        }
        
        Map<String, Object> stats = new HashMap<>();
        stats.put("pendingNotifications", pendingCount);
        stats.put("sentNotifications", sentCount);
        stats.put("failedNotifications", failedCount);
        stats.put("channelStats", channelCounts);
        stats.put("startDate", startDate);
        stats.put("endDate", endDate);
        
        return stats;
    }
    
    private void validateNotificationRequest(NotificationRequest request) {
        if (request.getUserId() == null && request.getEmail() == null && 
            request.getPhone() == null && request.getDeviceToken() == null) {
            throw new RuntimeException("At least one recipient (userId, email, phone, or deviceToken) is required");
        }
        
        if (request.getTemplateName() == null && request.getContent() == null) {
            throw new RuntimeException("Either template name or content is required");
        }
    }
    
    private void processTemplate(Notification notification, Map<String, Object> variables) {
        try {
            Map<String, Object> templateResult = templateService.processTemplate(
                    notification.getTemplateName(), 
                    notification.getType(), 
                    variables
            );
            
            if (templateResult.containsKey("subject")) {
                notification.setSubject((String) templateResult.get("subject"));
            }
            if (templateResult.containsKey("content")) {
                notification.setContent((String) templateResult.get("content"));
            }
            
        } catch (Exception e) {
            log.warn("Template processing failed for {}: {}", notification.getTemplateName(), e.getMessage());
            // Continue without template if processing fails
        }
    }
    
    private boolean sendNotificationByType(Notification notification) {
        try {
            switch (notification.getType().toUpperCase()) {
                case "EMAIL":
                    return emailService.sendEmail(notification);
                case "SMS":
                    return smsService.sendSms(notification);
                case "PUSH":
                    return pushNotificationService.sendPushNotification(notification);
                default:
                    throw new RuntimeException("Unsupported notification type: " + notification.getType());
            }
        } catch (Exception e) {
            log.error("Failed to send {} notification: {}", notification.getType(), notification.getNotificationId(), e);
            notification.setFailureReason(e.getMessage());
            return false;
        }
    }
    
    private void mapNotificationRequestToEntity(NotificationRequest request, Notification notification) {
        notification.setUserId(request.getUserId());
        notification.setEmail(request.getEmail());
        notification.setPhone(request.getPhone());
        notification.setDeviceToken(request.getDeviceToken());
        notification.setType(request.getType());
        notification.setChannel(request.getChannel());
        notification.setTemplateName(request.getTemplateName());
        notification.setSubject(request.getSubject());
        notification.setContent(request.getContent());
        notification.setPriority(request.getPriority());
        notification.setReferenceType(request.getReferenceType());
        notification.setReferenceId(request.getReferenceId());
        
        if (request.getMetadata() != null) {
            // Convert metadata map to JSON string
            notification.setMetadata(convertMapToJson(request.getMetadata()));
        }
    }
    
    private NotificationResponse mapNotificationToResponse(Notification notification) {
        NotificationResponse response = new NotificationResponse();
        response.setNotificationId(notification.getNotificationId());
        response.setUserId(notification.getUserId());
        response.setEmail(notification.getEmail());
        response.setPhone(notification.getPhone());
        response.setType(notification.getType());
        response.setChannel(notification.getChannel());
        response.setTemplateName(notification.getTemplateName());
        response.setSubject(notification.getSubject());
        response.setContent(notification.getContent());
        response.setStatus(notification.getStatus());
        response.setPriority(notification.getPriority());
        response.setReferenceType(notification.getReferenceType());
        response.setReferenceId(notification.getReferenceId());
        response.setFailureReason(notification.getFailureReason());
        response.setRetryCount(notification.getRetryCount());
        response.setCreatedAt(notification.getCreatedAt());
        response.setSentAt(notification.getSentAt());
        response.setDeliveredAt(notification.getDeliveredAt());
        response.setReadAt(notification.getReadAt());
        
        if (notification.getMetadata() != null) {
            // Convert metadata JSON string to map
            response.setMetadata(convertJsonToMap(notification.getMetadata()));
        }
        
        return response;
    }
    
    private String convertMapToJson(Map<String, Object> map) {
        try {
            // Simple JSON conversion - in production use Jackson ObjectMapper
            StringBuilder json = new StringBuilder("{");
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                json.append("\"").append(entry.getKey()).append("\":\"")
                    .append(entry.getValue()).append("\",");
            }
            if (json.length() > 1) {
                json.deleteCharAt(json.length() - 1);
            }
            json.append("}");
            return json.toString();
        } catch (Exception e) {
            log.warn("Failed to convert map to JSON: {}", e.getMessage());
            return "{}";
        }
    }
    
    private Map<String, Object> convertJsonToMap(String json) {
        try {
            // Simple JSON parsing - in production use Jackson ObjectMapper
            Map<String, Object> map = new HashMap<>();
            if (json != null && json.startsWith("{") && json.endsWith("}")) {
                String content = json.substring(1, json.length() - 1);
                String[] pairs = content.split(",");
                for (String pair : pairs) {
                    String[] keyValue = pair.split(":");
                    if (keyValue.length == 2) {
                        String key = keyValue[0].replace("\"", "").trim();
                        String value = keyValue[1].replace("\"", "").trim();
                        map.put(key, value);
                    }
                }
            }
            return map;
        } catch (Exception e) {
            log.warn("Failed to convert JSON to map: {}", e.getMessage());
            return new HashMap<>();
        }
    }
}