Overview
Stock Level Alerts system monitors inventory levels and sends notifications when stock reaches predefined thresholds. This implementation includes real-time monitoring, multi-channel notifications, and comprehensive alert management.
1. Dependencies
<dependencies> <!-- Spring Boot for Web & Scheduling --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> <version>3.1.0</version> </dependency> <!-- Database --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>2.15.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> <!-- Email --> <dependency> <groupId>com.sun.mail</groupId> <artifactId>javax.mail</artifactId> <version>1.6.2</version> </dependency> <!-- SMS (Twilio) --> <dependency> <groupId>com.twilio.sdk</groupId> <artifactId>twilio</artifactId> <version>9.10.0</version> </dependency> <!-- WebSocket for real-time notifications --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> <version>3.1.0</version> </dependency> <!-- Caching --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> <version>3.1.0</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> </dependency> </dependencies>
2. Core Domain Models
Stock and Alert Models
package com.example.stockalerts.model; import javax.persistence.*; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Map; @Entity @Table(name = "products") public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(unique = true, nullable = false) private String sku; @Column(nullable = false) private String name; private String description; private String category; private String supplier; @Column(name = "current_stock") private Integer currentStock; @Column(name = "minimum_stock_level") private Integer minimumStockLevel; @Column(name = "maximum_stock_level") private Integer maximumStockLevel; @Column(name = "reorder_point") private Integer reorderPoint; @Column(name = "safety_stock") private Integer safetyStock; private BigDecimal cost; private BigDecimal price; @Column(name = "is_active") private Boolean isActive = true; @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getSku() { return sku; } public void setSku(String sku) { this.sku = sku; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public String getCategory() { return category; } public void setCategory(String category) { this.category = category; } public String getSupplier() { return supplier; } public void setSupplier(String supplier) { this.supplier = supplier; } public Integer getCurrentStock() { return currentStock; } public void setCurrentStock(Integer currentStock) { this.currentStock = currentStock; } public Integer getMinimumStockLevel() { return minimumStockLevel; } public void setMinimumStockLevel(Integer minimumStockLevel) { this.minimumStockLevel = minimumStockLevel; } public Integer getMaximumStockLevel() { return maximumStockLevel; } public void setMaximumStockLevel(Integer maximumStockLevel) { this.maximumStockLevel = maximumStockLevel; } public Integer getReorderPoint() { return reorderPoint; } public void setReorderPoint(Integer reorderPoint) { this.reorderPoint = reorderPoint; } public Integer getSafetyStock() { return safetyStock; } public void setSafetyStock(Integer safetyStock) { this.safetyStock = safetyStock; } public BigDecimal getCost() { return cost; } public void setCost(BigDecimal cost) { this.cost = cost; } public BigDecimal getPrice() { return price; } public void setPrice(BigDecimal price) { this.price = price; } public Boolean getIsActive() { return isActive; } public void setIsActive(Boolean isActive) { this.isActive = isActive; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } // Business logic methods public boolean isBelowMinimum() { return currentStock <= minimumStockLevel; } public boolean isBelowReorderPoint() { return currentStock <= reorderPoint; } public boolean isAboveMaximum() { return currentStock >= maximumStockLevel; } public boolean needsReorder() { return currentStock <= reorderPoint && currentStock > minimumStockLevel; } public boolean isOutOfStock() { return currentStock <= 0; } public Integer getStockDeficit() { return Math.max(0, safetyStock - currentStock); } } @Entity @Table(name = "stock_alerts") public class StockAlert { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "product_id", nullable = false) private Product product; @Enumerated(EnumType.STRING) @Column(nullable = false) private AlertType alertType; @Enumerated(EnumType.STRING) @Column(nullable = false) private AlertSeverity severity; @Column(nullable = false) private String message; @Column(name = "current_stock") private Integer currentStock; @Column(name = "threshold_stock") private Integer thresholdStock; @Enumerated(EnumType.STRING) @Column(nullable = false) private AlertStatus status = AlertStatus.PENDING; @Column(name = "triggered_at") private LocalDateTime triggeredAt; @Column(name = "acknowledged_at") private LocalDateTime acknowledgedAt; @Column(name = "resolved_at") private LocalDateTime resolvedAt; private String acknowledgedBy; private String resolvedBy; @Column(name = "notification_sent") private Boolean notificationSent = false; @ElementCollection @CollectionTable(name = "alert_metadata", joinColumns = @JoinColumn(name = "alert_id")) @MapKeyColumn(name = "meta_key") @Column(name = "meta_value") private Map<String, String> metadata; @PrePersist protected void onCreate() { triggeredAt = LocalDateTime.now(); } // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Product getProduct() { return product; } public void setProduct(Product product) { this.product = product; } public AlertType getAlertType() { return alertType; } public void setAlertType(AlertType alertType) { this.alertType = alertType; } public AlertSeverity getSeverity() { return severity; } public void setSeverity(AlertSeverity severity) { this.severity = severity; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public Integer getCurrentStock() { return currentStock; } public void setCurrentStock(Integer currentStock) { this.currentStock = currentStock; } public Integer getThresholdStock() { return thresholdStock; } public void setThresholdStock(Integer thresholdStock) { this.thresholdStock = thresholdStock; } public AlertStatus getStatus() { return status; } public void setStatus(AlertStatus status) { this.status = status; } public LocalDateTime getTriggeredAt() { return triggeredAt; } public void setTriggeredAt(LocalDateTime triggeredAt) { this.triggeredAt = triggeredAt; } public LocalDateTime getAcknowledgedAt() { return acknowledgedAt; } public void setAcknowledgedAt(LocalDateTime acknowledgedAt) { this.acknowledgedAt = acknowledgedAt; } public LocalDateTime getResolvedAt() { return resolvedAt; } public void setResolvedAt(LocalDateTime resolvedAt) { this.resolvedAt = resolvedAt; } public String getAcknowledgedBy() { return acknowledgedBy; } public void setAcknowledgedBy(String acknowledgedBy) { this.acknowledgedBy = acknowledgedBy; } public String getResolvedBy() { return resolvedBy; } public void setResolvedBy(String resolvedBy) { this.resolvedBy = resolvedBy; } public Boolean getNotificationSent() { return notificationSent; } public void setNotificationSent(Boolean notificationSent) { this.notificationSent = notificationSent; } public Map<String, String> getMetadata() { return metadata; } public void setMetadata(Map<String, String> metadata) { this.metadata = metadata; } // Business methods public void acknowledge(String user) { this.status = AlertStatus.ACKNOWLEDGED; this.acknowledgedAt = LocalDateTime.now(); this.acknowledgedBy = user; } public void resolve(String user) { this.status = AlertStatus.RESOLVED; this.resolvedAt = LocalDateTime.now(); this.resolvedBy = user; } public boolean isActive() { return status == AlertStatus.PENDING || status == AlertStatus.ACKNOWLEDGED; } } @Entity @Table(name = "alert_configurations") public class AlertConfiguration { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false) private String name; private String description; @Enumerated(EnumType.STRING) @Column(nullable = false) private AlertType alertType; @Column(name = "is_enabled") private Boolean isEnabled = true; @Column(name = "minimum_stock_threshold") private Integer minimumStockThreshold; @Column(name = "reorder_point_threshold") private Integer reorderPointThreshold; @Column(name = "maximum_stock_threshold") private Integer maximumStockThreshold; @Column(name = "safety_stock_threshold") private Integer safetyStockThreshold; @Enumerated(EnumType.STRING) private AlertSeverity severity; @ElementCollection @CollectionTable(name = "alert_recipients", joinColumns = @JoinColumn(name = "config_id")) @Column(name = "recipient_email") private java.util.Set<String> emailRecipients; @ElementCollection @CollectionTable(name = "alert_phone_recipients", joinColumns = @JoinColumn(name = "config_id")) @Column(name = "recipient_phone") private java.util.Set<String> phoneRecipients; @Column(name = "notification_channels") @Enumerated(EnumType.STRING) @ElementCollection(fetch = FetchType.EAGER) private java.util.Set<NotificationChannel> notificationChannels; @Column(name = "cooldown_minutes") private Integer cooldownMinutes = 60; // Prevent duplicate alerts @Column(name = "created_at") private LocalDateTime createdAt; @Column(name = "updated_at") private LocalDateTime updatedAt; @PrePersist protected void onCreate() { createdAt = LocalDateTime.now(); updatedAt = LocalDateTime.now(); } @PreUpdate protected void onUpdate() { updatedAt = LocalDateTime.now(); } // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } public AlertType getAlertType() { return alertType; } public void setAlertType(AlertType alertType) { this.alertType = alertType; } public Boolean getIsEnabled() { return isEnabled; } public void setIsEnabled(Boolean isEnabled) { this.isEnabled = isEnabled; } public Integer getMinimumStockThreshold() { return minimumStockThreshold; } public void setMinimumStockThreshold(Integer minimumStockThreshold) { this.minimumStockThreshold = minimumStockThreshold; } public Integer getReorderPointThreshold() { return reorderPointThreshold; } public void setReorderPointThreshold(Integer reorderPointThreshold) { this.reorderPointThreshold = reorderPointThreshold; } public Integer getMaximumStockThreshold() { return maximumStockThreshold; } public void setMaximumStockThreshold(Integer maximumStockThreshold) { this.maximumStockThreshold = maximumStockThreshold; } public Integer getSafetyStockThreshold() { return safetyStockThreshold; } public void setSafetyStockThreshold(Integer safetyStockThreshold) { this.safetyStockThreshold = safetyStockThreshold; } public AlertSeverity getSeverity() { return severity; } public void setSeverity(AlertSeverity severity) { this.severity = severity; } public java.util.Set<String> getEmailRecipients() { return emailRecipients; } public void setEmailRecipients(java.util.Set<String> emailRecipients) { this.emailRecipients = emailRecipients; } public java.util.Set<String> getPhoneRecipients() { return phoneRecipients; } public void setPhoneRecipients(java.util.Set<String> phoneRecipients) { this.phoneRecipients = phoneRecipients; } public java.util.Set<NotificationChannel> getNotificationChannels() { return notificationChannels; } public void setNotificationChannels(java.util.Set<NotificationChannel> notificationChannels) { this.notificationChannels = notificationChannels; } public Integer getCooldownMinutes() { return cooldownMinutes; } public void setCooldownMinutes(Integer cooldownMinutes) { this.cooldownMinutes = cooldownMinutes; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } public LocalDateTime getUpdatedAt() { return updatedAt; } public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } } // Enums public enum AlertType { LOW_STOCK("Low Stock"), OUT_OF_STOCK("Out of Stock"), REORDER_POINT("Reorder Point"), OVERSTOCK("Overstock"), EXPIRING_SOON("Expiring Soon"), EXPIRED("Expired"), THEFT_SUSPICION("Theft Suspicion"), ABNORMAL_CONSUMPTION("Abnormal Consumption"); private final String displayName; AlertType(String displayName) { this.displayName = displayName; } public String getDisplayName() { return displayName; } } public enum AlertSeverity { LOW("Low"), MEDIUM("Medium"), HIGH("High"), CRITICAL("Critical"); private final String displayName; AlertSeverity(String displayName) { this.displayName = displayName; } public String getDisplayName() { return displayName; } } public enum AlertStatus { PENDING("Pending"), ACKNOWLEDGED("Acknowledged"), RESOLVED("Resolved"), SUPPRESSED("Suppressed"); private final String displayName; AlertStatus(String displayName) { this.displayName = displayName; } public String getDisplayName() { return displayName; } } public enum NotificationChannel { EMAIL("Email"), SMS("SMS"), PUSH("Push Notification"), SLACK("Slack"), WEBHOOK("Webhook"), DASHBOARD("Dashboard"); private final String displayName; NotificationChannel(String displayName) { this.displayName = displayName; } public String getDisplayName() { return displayName; } } 3. Alert Detection Engine
package com.example.stockalerts.engine; import com.example.stockalerts.model.*; import com.example.stockalerts.repository.ProductRepository; import com.example.stockalerts.repository.AlertConfigurationRepository; import com.example.stockalerts.repository.StockAlertRepository; import com.example.stockalerts.service.NotificationService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import javax.transaction.Transactional; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; @Component public class AlertDetectionEngine { private static final Logger log = LoggerFactory.getLogger(AlertDetectionEngine.class); @Autowired private ProductRepository productRepository; @Autowired private AlertConfigurationRepository alertConfigRepository; @Autowired private StockAlertRepository stockAlertRepository; @Autowired private NotificationService notificationService; private final Map<Long, LocalDateTime> lastAlertTimeCache = new HashMap<>(); /** * Scan all products for stock level alerts */ @Scheduled(fixedRate = 300000) // Run every 5 minutes @Transactional public void scanForAlerts() { log.info("Starting stock level alert scan..."); List<Product> products = productRepository.findByIsActiveTrue(); List<AlertConfiguration> configurations = alertConfigRepository.findByIsEnabledTrue(); int alertCount = 0; for (Product product : products) { for (AlertConfiguration config : configurations) { if (shouldCheckProduct(product, config)) { Optional<StockAlert> alert = checkForAlert(product, config); if (alert.isPresent()) { stockAlertRepository.save(alert.get()); notificationService.sendAlertNotification(alert.get()); alertCount++; } } } } log.info("Stock level alert scan completed. Generated {} new alerts.", alertCount); } /** * Check specific product for alerts based on configuration */ public List<StockAlert> checkProductForAlerts(Product product) { List<AlertConfiguration> configurations = alertConfigRepository.findByIsEnabledTrue(); List<StockAlert> alerts = new ArrayList<>(); for (AlertConfiguration config : configurations) { if (shouldCheckProduct(product, config)) { checkForAlert(product, config).ifPresent(alerts::add); } } return alerts; } /** * Check if a product should be evaluated for a specific alert configuration */ private boolean shouldCheckProduct(Product product, AlertConfiguration config) { // Check if we're in cooldown period for this product and alert type String cacheKey = generateCacheKey(product.getId(), config.getAlertType()); LocalDateTime lastAlertTime = lastAlertTimeCache.get(cacheKey.hashCode()); if (lastAlertTime != null) { LocalDateTime cooldownUntil = lastAlertTime.plusMinutes(config.getCooldownMinutes()); if (LocalDateTime.now().isBefore(cooldownUntil)) { return false; } } return true; } /** * Check for specific alert condition */ private Optional<StockAlert> checkForAlert(Product product, AlertConfiguration config) { AlertType alertType = config.getAlertType(); boolean conditionMet = false; String message = ""; Integer threshold = null; switch (alertType) { case LOW_STOCK: conditionMet = product.isBelowMinimum(); threshold = product.getMinimumStockLevel(); message = String.format("Product '%s' is below minimum stock level. Current: %d, Minimum: %d", product.getName(), product.getCurrentStock(), product.getMinimumStockLevel()); break; case OUT_OF_STOCK: conditionMet = product.isOutOfStock(); threshold = 0; message = String.format("Product '%s' is out of stock", product.getName()); break; case REORDER_POINT: conditionMet = product.needsReorder(); threshold = product.getReorderPoint(); message = String.format("Product '%s' has reached reorder point. Current: %d, Reorder: %d", product.getName(), product.getCurrentStock(), product.getReorderPoint()); break; case OVERSTOCK: conditionMet = product.isAboveMaximum(); threshold = product.getMaximumStockLevel(); message = String.format("Product '%s' is above maximum stock level. Current: %d, Maximum: %d", product.getName(), product.getCurrentStock(), product.getMaximumStockLevel()); break; case ABNORMAL_CONSUMPTION: conditionMet = checkAbnormalConsumption(product, config); message = String.format("Abnormal consumption detected for product '%s'", product.getName()); break; } if (conditionMet) { // Check if similar alert already exists and is active if (!hasActiveSimilarAlert(product, alertType)) { StockAlert alert = createAlert(product, config, message, threshold); updateCooldownCache(product.getId(), alertType); return Optional.of(alert); } } return Optional.empty(); } /** * Check for abnormal consumption patterns */ private boolean checkAbnormalConsumption(Product product, AlertConfiguration config) { // This would typically involve analyzing historical consumption data // For simplicity, we'll use a basic threshold-based approach Integer safetyThreshold = config.getSafetyStockThreshold(); if (safetyThreshold != null) { return product.getCurrentStock() < safetyThreshold; } return false; } /** * Check if there's already an active alert of the same type for this product */ private boolean hasActiveSimilarAlert(Product product, AlertType alertType) { List<StockAlert> activeAlerts = stockAlertRepository.findByProductAndStatusIn( product, Arrays.asList(AlertStatus.PENDING, AlertStatus.ACKNOWLEDGED)); return activeAlerts.stream() .anyMatch(alert -> alert.getAlertType() == alertType); } /** * Create a new stock alert */ private StockAlert createAlert(Product product, AlertConfiguration config, String message, Integer threshold) { StockAlert alert = new StockAlert(); alert.setProduct(product); alert.setAlertType(config.getAlertType()); alert.setSeverity(config.getSeverity()); alert.setMessage(message); alert.setCurrentStock(product.getCurrentStock()); alert.setThresholdStock(threshold); alert.setStatus(AlertStatus.PENDING); // Add metadata Map<String, String> metadata = new HashMap<>(); metadata.put("product_sku", product.getSku()); metadata.put("product_category", product.getCategory()); metadata.put("supplier", product.getSupplier()); metadata.put("config_id", config.getId().toString()); alert.setMetadata(metadata); return alert; } /** * Update cooldown cache to prevent duplicate alerts */ private void updateCooldownCache(Long productId, AlertType alertType) { String cacheKey = generateCacheKey(productId, alertType); lastAlertTimeCache.put(cacheKey.hashCode(), LocalDateTime.now()); // Clean up old cache entries (older than 24 hours) LocalDateTime twentyFourHoursAgo = LocalDateTime.now().minusHours(24); lastAlertTimeCache.entrySet().removeIf(entry -> entry.getValue().isBefore(twentyFourHoursAgo)); } private String generateCacheKey(Long productId, AlertType alertType) { return productId + "_" + alertType.name(); } /** * Manually trigger alert check for specific products */ @Transactional public List<StockAlert> triggerManualScan(List<Long> productIds) { log.info("Manual alert scan triggered for {} products", productIds.size()); List<Product> products = productRepository.findAllById(productIds); List<StockAlert> generatedAlerts = new ArrayList<>(); for (Product product : products) { List<StockAlert> alerts = checkProductForAlerts(product); stockAlertRepository.saveAll(alerts); alerts.forEach(notificationService::sendAlertNotification); generatedAlerts.addAll(alerts); } log.info("Manual scan completed. Generated {} alerts.", generatedAlerts.size()); return generatedAlerts; } /** * Get alert statistics */ public AlertStatistics getAlertStatistics() { AlertStatistics stats = new AlertStatistics(); stats.setTotalAlerts(stockAlertRepository.count()); stats.setPendingAlerts(stockAlertRepository.countByStatus(AlertStatus.PENDING)); stats.setCriticalAlerts(stockAlertRepository.countBySeverity(AlertSeverity.CRITICAL)); // Get alerts by type Map<AlertType, Long> alertsByType = Arrays.stream(AlertType.values()) .collect(Collectors.toMap( type -> type, type -> stockAlertRepository.countByAlertTypeAndStatusIn( type, Arrays.asList(AlertStatus.PENDING, AlertStatus.ACKNOWLEDGED)) )); stats.setAlertsByType(alertsByType); // Get top products with most alerts List<Object[]> topProducts = stockAlertRepository.findTopProductsWithAlerts(10); stats.setTopAlertedProducts(topProducts.stream() .collect(Collectors.toMap( arr -> (String) arr[0], // product name arr -> (Long) arr[1] // alert count ))); return stats; } } class AlertStatistics { private Long totalAlerts; private Long pendingAlerts; private Long criticalAlerts; private Map<AlertType, Long> alertsByType; private Map<String, Long> topAlertedProducts; // Getters and Setters public Long getTotalAlerts() { return totalAlerts; } public void setTotalAlerts(Long totalAlerts) { this.totalAlerts = totalAlerts; } public Long getPendingAlerts() { return pendingAlerts; } public void setPendingAlerts(Long pendingAlerts) { this.pendingAlerts = pendingAlerts; } public Long getCriticalAlerts() { return criticalAlerts; } public void setCriticalAlerts(Long criticalAlerts) { this.criticalAlerts = criticalAlerts; } public Map<AlertType, Long> getAlertsByType() { return alertsByType; } public void setAlertsByType(Map<AlertType, Long> alertsByType) { this.alertsByType = alertsByType; } public Map<String, Long> getTopAlertedProducts() { return topAlertedProducts; } public void setTopAlertedProducts(Map<String, Long> topAlertedProducts) { this.topAlertedProducts = topAlertedProducts; } } 4. Notification Service
package com.example.stockalerts.service; import com.example.stockalerts.model.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.util.Set; @Service public class NotificationService { private static final Logger log = LoggerFactory.getLogger(NotificationService.class); @Autowired private JavaMailSender mailSender; @Autowired private RestTemplate restTemplate; @Autowired private SMSService smsService; @Autowired private WebSocketNotificationService webSocketService; /** * Send alert notification through configured channels */ public void sendAlertNotification(StockAlert alert) { AlertConfiguration config = alert.getProduct().getAlertConfiguration(); // This would need to be implemented if (config == null) { log.warn("No alert configuration found for alert: {}", alert.getId()); return; } Set<NotificationChannel> channels = config.getNotificationChannels(); for (NotificationChannel channel : channels) { try { switch (channel) { case EMAIL: sendEmailNotification(alert, config); break; case SMS: sendSMSNotification(alert, config); break; case DASHBOARD: sendDashboardNotification(alert); break; case SLACK: sendSlackNotification(alert, config); break; case WEBHOOK: sendWebhookNotification(alert, config); break; } } catch (Exception e) { log.error("Failed to send {} notification for alert: {}", channel, alert.getId(), e); } } alert.setNotificationSent(true); } /** * Send email notification */ private void sendEmailNotification(StockAlert alert, AlertConfiguration config) { if (config.getEmailRecipients() == null || config.getEmailRecipients().isEmpty()) { log.warn("No email recipients configured for alert: {}", alert.getId()); return; } try { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true); helper.setTo(config.getEmailRecipients().toArray(new String[0])); helper.setSubject(buildEmailSubject(alert)); helper.setText(buildEmailContent(alert), true); mailSender.send(message); log.info("Email notification sent for alert: {}", alert.getId()); } catch (MessagingException e) { log.error("Failed to send email notification for alert: {}", alert.getId(), e); throw new RuntimeException("Email sending failed", e); } } /** * Send SMS notification */ private void sendSMSNotification(StockAlert alert, AlertConfiguration config) { if (config.getPhoneRecipients() == null || config.getPhoneRecipients().isEmpty()) { log.warn("No phone recipients configured for alert: {}", alert.getId()); return; } String message = buildSMSContent(alert); for (String phoneNumber : config.getPhoneRecipients()) { try { smsService.sendSMS(phoneNumber, message); log.info("SMS notification sent to {} for alert: {}", phoneNumber, alert.getId()); } catch (Exception e) { log.error("Failed to send SMS to {} for alert: {}", phoneNumber, alert.getId(), e); } } } /** * Send dashboard notification (WebSocket) */ private void sendDashboardNotification(StockAlert alert) { webSocketService.sendAlertNotification(alert); log.info("Dashboard notification sent for alert: {}", alert.getId()); } /** * Send Slack notification */ private void sendSlackNotification(StockAlert alert, AlertConfiguration config) { // Implementation for Slack webhook String webhookUrl = "https://hooks.slack.com/services/your-webhook-url"; SlackMessage slackMessage = buildSlackMessage(alert); try { restTemplate.postForEntity(webhookUrl, slackMessage, String.class); log.info("Slack notification sent for alert: {}", alert.getId()); } catch (Exception e) { log.error("Failed to send Slack notification for alert: {}", alert.getId(), e); } } /** * Send webhook notification */ private void sendWebhookNotification(StockAlert alert, AlertConfiguration config) { // Implementation for generic webhook String webhookUrl = config.getMetadata().get("webhook_url"); // Store in metadata if (webhookUrl != null) { try { restTemplate.postForEntity(webhookUrl, alert, String.class); log.info("Webhook notification sent for alert: {}", alert.getId()); } catch (Exception e) { log.error("Failed to send webhook notification for alert: {}", alert.getId(), e); } } } /** * Build email subject */ private String buildEmailSubject(StockAlert alert) { return String.format("[%s] Stock Alert: %s - %s", alert.getSeverity().getDisplayName(), alert.getAlertType().getDisplayName(), alert.getProduct().getName()); } /** * Build email content (HTML) */ private String buildEmailContent(StockAlert alert) { return String.format(""" <html> <body> <h2>Stock Level Alert</h2> <div style="border-left: 4px solid %s; padding-left: 15px;"> <h3>%s</h3> <p><strong>Product:</strong> %s (SKU: %s)</p> <p><strong>Current Stock:</strong> %d</p> <p><strong>Threshold:</strong> %d</p> <p><strong>Alert Type:</strong> %s</p> <p><strong>Severity:</strong> %s</p> <p><strong>Time:</strong> %s</p> </div> <br> <p>Please take appropriate action.</p> <p><a href="http://your-dashboard-url/alerts/%d">View in Dashboard</a></p> </body> </html> """, getSeverityColor(alert.getSeverity()), alert.getMessage(), alert.getProduct().getName(), alert.getProduct().getSku(), alert.getCurrentStock(), alert.getThresholdStock(), alert.getAlertType().getDisplayName(), alert.getSeverity().getDisplayName(), alert.getTriggeredAt(), alert.getId() ); } /** * Build SMS content */ private String buildSMSContent(StockAlert alert) { return String.format("ALERT: %s - %s. Current: %d, Threshold: %d. %s", alert.getAlertType().getDisplayName(), alert.getProduct().getName(), alert.getCurrentStock(), alert.getThresholdStock(), alert.getMessage()); } /** * Build Slack message */ private SlackMessage buildSlackMessage(StockAlert alert) { SlackMessage message = new SlackMessage(); message.setText(String.format("Stock Alert: %s", alert.getMessage())); SlackAttachment attachment = new SlackAttachment(); attachment.setColor(getSeverityColor(alert.getSeverity())); attachment.setTitle("Stock Level Alert"); attachment.setText(alert.getMessage()); attachment.addField("Product", alert.getProduct().getName(), true); attachment.addField("Current Stock", String.valueOf(alert.getCurrentStock()), true); attachment.addField("Alert Type", alert.getAlertType().getDisplayName(), true); attachment.addField("Severity", alert.getSeverity().getDisplayName(), true); message.addAttachment(attachment); return message; } /** * Get color based on severity */ private String getSeverityColor(AlertSeverity severity) { switch (severity) { case LOW: return "#3498db"; // Blue case MEDIUM: return "#f39c12"; // Orange case HIGH: return "#e74c3c"; // Red case CRITICAL: return "#8b0000"; // Dark Red default: return "#95a5a6"; // Gray } } } // SMS Service @Service class SMSService { private static final Logger log = LoggerFactory.getLogger(SMSService.class); // In production, you would use Twilio, AWS SNS, or similar service public void sendSMS(String phoneNumber, String message) { // Mock implementation - replace with actual SMS provider log.info("SMS sent to {}: {}", phoneNumber, message); // Example with Twilio: /* Twilio.init(accountSid, authToken); Message.creator( new PhoneNumber(phoneNumber), new PhoneNumber("+1234567890"), // Your Twilio number message ).create(); */ } } // WebSocket Notification Service @Service class WebSocketNotificationService { public void sendAlertNotification(StockAlert alert) { // Implementation would use SimpMessagingTemplate to send to WebSocket clients // simpMessagingTemplate.convertAndSend("/topic/alerts", alert); } } // Slack message classes class SlackMessage { private String text; private java.util.List<SlackAttachment> attachments; // Getters and Setters public String getText() { return text; } public void setText(String text) { this.text = text; } public java.util.List<SlackAttachment> getAttachments() { return attachments; } public void setAttachments(java.util.List<SlackAttachment> attachments) { this.attachments = attachments; } public void addAttachment(SlackAttachment attachment) { if (attachments == null) { attachments = new java.util.ArrayList<>(); } attachments.add(attachment); } } class SlackAttachment { private String color; private String title; private String text; private java.util.List<SlackField> fields; // Getters and Setters public String getColor() { return color; } public void setColor(String color) { this.color = color; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getText() { return text; } public void setText(String text) { this.text = text; } public java.util.List<SlackField> getFields() { return fields; } public void setFields(java.util.List<SlackField> fields) { this.fields = fields; } public void addField(String title, String value, boolean Short) { if (fields == null) { fields = new java.util.ArrayList<>(); } SlackField field = new SlackField(); field.setTitle(title); field.setValue(value); field.setShort(Short); fields.add(field); } } class SlackField { private String title; private String value; private boolean Short; // Getters and Setters public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getValue() { return value; } public void setValue(String value) { this.value = value; } public boolean isShort() { return Short; } public void setShort(boolean aShort) { Short = aShort; } } 5. Repository Layer
package com.example.stockalerts.repository; import com.example.stockalerts.model.*; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface ProductRepository extends JpaRepository<Product, Long> { List<Product> findByIsActiveTrue(); List<Product> findByCurrentStockLessThanEqual(Integer threshold); List<Product> findByCurrentStockGreaterThanEqual(Integer threshold); List<Product> findByCategoryAndIsActiveTrue(String category); Product findBySku(String sku); @Query("SELECT p FROM Product p WHERE p.currentStock <= p.minimumStockLevel AND p.isActive = true") List<Product> findProductsBelowMinimumStock(); @Query("SELECT p FROM Product p WHERE p.currentStock <= p.reorderPoint AND p.isActive = true") List<Product> findProductsAtReorderPoint(); @Query("SELECT p FROM Product p WHERE p.currentStock = 0 AND p.isActive = true") List<Product> findOutOfStockProducts(); } @Repository public interface StockAlertRepository extends JpaRepository<StockAlert, Long> { List<StockAlert> findByStatusIn(List<AlertStatus> statuses); List<StockAlert> findByProductAndStatusIn(Product product, List<AlertStatus> statuses); List<StockAlert> findByAlertTypeAndStatusIn(AlertType alertType, List<AlertStatus> statuses); List<StockAlert> findBySeverityAndStatusIn(AlertSeverity severity, List<AlertStatus> statuses); Long countByStatus(AlertStatus status); Long countBySeverity(AlertSeverity severity); Long countByAlertTypeAndStatusIn(AlertType alertType, List<AlertStatus> statuses); @Query("SELECT sa FROM StockAlert sa WHERE sa.product.id = :productId AND sa.status IN :statuses") List<StockAlert> findByProductIdAndStatusIn(@Param("productId") Long productId, @Param("statuses") List<AlertStatus> statuses); @Query("SELECT p.name, COUNT(sa) FROM StockAlert sa JOIN sa.product p " + "WHERE sa.status IN ('PENDING', 'ACKNOWLEDGED') " + "GROUP BY p.id, p.name " + "ORDER BY COUNT(sa) DESC LIMIT :limit") List<Object[]> findTopProductsWithAlerts(@Param("limit") int limit); @Query("SELECT sa FROM StockAlert sa WHERE sa.triggeredAt >= :since AND sa.status IN :statuses") List<StockAlert> findRecentAlerts(@Param("since") java.time.LocalDateTime since, @Param("statuses") List<AlertStatus> statuses); } @Repository public interface AlertConfigurationRepository extends JpaRepository<AlertConfiguration, Long> { List<AlertConfiguration> findByIsEnabledTrue(); List<AlertConfiguration> findByAlertTypeAndIsEnabledTrue(AlertType alertType); List<AlertConfiguration> findByNotificationChannelsContains(NotificationChannel channel); } 6. REST API Controllers
package com.example.stockalerts.controller; import com.example.stockalerts.engine.AlertDetectionEngine; import com.example.stockalerts.engine.AlertStatistics; import com.example.stockalerts.model.*; import com.example.stockalerts.repository.StockAlertRepository; import com.example.stockalerts.service.AlertManagementService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime; import java.util.List; import java.util.Map; @RestController @RequestMapping("/api/alerts") public class AlertController { @Autowired private StockAlertRepository alertRepository; @Autowired private AlertDetectionEngine alertDetectionEngine; @Autowired private AlertManagementService alertManagementService; /** * Get all alerts with pagination and filtering */ @GetMapping public ResponseEntity<Page<StockAlert>> getAlerts( @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, @RequestParam(required = false) AlertStatus status, @RequestParam(required = false) AlertType type, @RequestParam(required = false) AlertSeverity severity, @RequestParam(required = false) Long productId, @RequestParam(defaultValue = "triggeredAt") String sortBy, @RequestParam(defaultValue = "DESC") String sortDirection) { Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy); Pageable pageable = PageRequest.of(page, size, sort); // In production, you would use Specification for complex filtering Page<StockAlert> alerts; if (status != null) { alerts = alertRepository.findByStatus(status, pageable); } else if (type != null && severity != null) { alerts = alertRepository.findByAlertTypeAndSeverity(type, severity, pageable); } else { alerts = alertRepository.findAll(pageable); } return ResponseEntity.ok(alerts); } /** * Get alert statistics */ @GetMapping("/statistics") public ResponseEntity<AlertStatistics> getAlertStatistics() { AlertStatistics statistics = alertDetectionEngine.getAlertStatistics(); return ResponseEntity.ok(statistics); } /** * Acknowledge an alert */ @PostMapping("/{alertId}/acknowledge") public ResponseEntity<StockAlert> acknowledgeAlert( @PathVariable Long alertId, @RequestParam String user) { StockAlert alert = alertManagementService.acknowledgeAlert(alertId, user); return ResponseEntity.ok(alert); } /** * Resolve an alert */ @PostMapping("/{alertId}/resolve") public ResponseEntity<StockAlert> resolveAlert( @PathVariable Long alertId, @RequestParam String user) { StockAlert alert = alertManagementService.resolveAlert(alertId, user); return ResponseEntity.ok(alert); } /** * Bulk acknowledge alerts */ @PostMapping("/bulk-acknowledge") public ResponseEntity<List<StockAlert>> bulkAcknowledgeAlerts( @RequestBody List<Long> alertIds, @RequestParam String user) { List<StockAlert> alerts = alertManagementService.bulkAcknowledgeAlerts(alertIds, user); return ResponseEntity.ok(alerts); } /** * Manually trigger alert scan */ @PostMapping("/scan") public ResponseEntity<List<StockAlert>> triggerManualScan(@RequestBody(required = false) List<Long> productIds) { List<StockAlert> alerts; if (productIds != null && !productIds.isEmpty()) { alerts = alertDetectionEngine.triggerManualScan(productIds); } else { // Scan all products alerts = alertDetectionEngine.triggerManualScan( alertRepository.findAll().stream().map(Product::getId).collect(java.util.stream.Collectors.toList()) ); } return ResponseEntity.ok(alerts); } /** * Get recent alerts (last 24 hours) */ @GetMapping("/recent") public ResponseEntity<List<StockAlert>> getRecentAlerts() { LocalDateTime since = LocalDateTime.now().minusHours(24); List<StockAlert> alerts = alertRepository.findRecentAlerts(since, java.util.Arrays.asList(AlertStatus.PENDING, AlertStatus.ACKNOWLEDGED)); return ResponseEntity.ok(alerts); } } @RestController @RequestMapping("/api/alert-configs") public class AlertConfigController { @Autowired private AlertConfigurationRepository configRepository; /** * Get all alert configurations */ @GetMapping public ResponseEntity<List<AlertConfiguration>> getConfigurations() { List<AlertConfiguration> configs = configRepository.findAll(); return ResponseEntity.ok(configs); } /** * Create new alert configuration */ @PostMapping public ResponseEntity<AlertConfiguration> createConfiguration(@RequestBody AlertConfiguration config) { AlertConfiguration savedConfig = configRepository.save(config); return ResponseEntity.ok(savedConfig); } /** * Update alert configuration */ @PutMapping("/{configId}") public ResponseEntity<AlertConfiguration> updateConfiguration( @PathVariable Long configId, @RequestBody AlertConfiguration config) { config.setId(configId); AlertConfiguration savedConfig = configRepository.save(config); return ResponseEntity.ok(savedConfig); } /** * Enable/disable alert configuration */ @PatchMapping("/{configId}/toggle") public ResponseEntity<AlertConfiguration> toggleConfiguration( @PathVariable Long configId, @RequestParam boolean enabled) { AlertConfiguration config = configRepository.findById(configId) .orElseThrow(() -> new RuntimeException("Configuration not found")); config.setIsEnabled(enabled); AlertConfiguration savedConfig = configRepository.save(config); return ResponseEntity.ok(savedConfig); } } 7. Alert Management Service
package com.example.stockalerts.service; import com.example.stockalerts.model.StockAlert; import com.example.stockalerts.repository.StockAlertRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.transaction.Transactional; import java.util.List; import java.util.stream.Collectors; @Service public class AlertManagementService { @Autowired private StockAlertRepository alertRepository; @Autowired private NotificationService notificationService; /** * Acknowledge an alert */ @Transactional public StockAlert acknowledgeAlert(Long alertId, String user) { StockAlert alert = alertRepository.findById(alertId) .orElseThrow(() -> new RuntimeException("Alert not found: " + alertId)); alert.acknowledge(user); StockAlert savedAlert = alertRepository.save(alert); // Send acknowledgment notification if needed notificationService.sendAcknowledgmentNotification(savedAlert); return savedAlert; } /** * Resolve an alert */ @Transactional public StockAlert resolveAlert(Long alertId, String user) { StockAlert alert = alertRepository.findById(alertId) .orElseThrow(() -> new RuntimeException("Alert not found: " + alertId)); alert.resolve(user); StockAlert savedAlert = alertRepository.save(alert); // Send resolution notification if needed notificationService.sendResolutionNotification(savedAlert); return savedAlert; } /** * Bulk acknowledge alerts */ @Transactional public List<StockAlert> bulkAcknowledgeAlerts(List<Long> alertIds, String user) { List<StockAlert> alerts = alertRepository.findAllById(alertIds); List<StockAlert> updatedAlerts = alerts.stream() .map(alert -> { alert.acknowledge(user); return alert; }) .collect(Collectors.toList()); List<StockAlert> savedAlerts = alertRepository.saveAll(updatedAlerts); // Send bulk acknowledgment notifications savedAlerts.forEach(notificationService::sendAcknowledgmentNotification); return savedAlerts; } /** * Auto-resolve alerts when stock is replenished */ @Transactional public void autoResolveAlertsForProduct(Long productId, String user) { List<StockAlert> activeAlerts = alertRepository.findByProductIdAndStatusIn( productId, java.util.Arrays.asList(AlertStatus.PENDING, AlertStatus.ACKNOWLEDGED)); List<StockAlert> resolvedAlerts = activeAlerts.stream() .map(alert -> { alert.resolve("System"); // Auto-resolve by system return alert; }) .collect(Collectors.toList()); alertRepository.saveAll(resolvedAlerts); // Log auto-resolution resolvedAlerts.forEach(alert -> System.out.println("Auto-resolved alert: " + alert.getId() + " for product: " + productId) ); } } 8. Configuration and Main Application
package com.example.stockalerts; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @SpringBootApplication @EnableScheduling @EnableCaching public class StockAlertsApplication { public static void main(String[] args) { SpringApplication.run(StockAlertsApplication.class, args); } @Bean public RestTemplate restTemplate() { return new RestTemplate(); } } // application.yml /* spring: datasource: url: jdbc:h2:mem:stockalerts driverClassName: org.h2.Driver username: sa password: jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create-drop show-sql: true h2: console: enabled: true mail: host: smtp.gmail.com port: 587 username: [email protected] password: your-app-password properties: mail: smtp: auth: true starttls: enable: true # Alert Configuration alerts: scan: enabled: true interval: 300000 # 5 minutes notifications: email: enabled: true sms: enabled: false slack: enabled: false webhook-url: ${SLACK_WEBHOOK_URL:} cooldown: default-minutes: 60 # Logging logging: level: com.example.stockalerts: DEBUG org.springframework.web: INFO */ 9. Example Usage
package com.example.stockalerts.demo; import com.example.stockalerts.engine.AlertDetectionEngine; import com.example.stockalerts.model.*; import com.example.stockalerts.repository.ProductRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.Arrays; @Component public class StockAlertsDemo implements CommandLineRunner { @Autowired private ProductRepository productRepository; @Autowired private AlertDetectionEngine alertDetectionEngine; @Override public void run(String... args) throws Exception { // Create sample products createSampleProducts(); // Trigger initial alert scan alertDetectionEngine.scanForAlerts(); } private void createSampleProducts() { // Product 1: Running low on stock Product product1 = new Product(); product1.setSku("LAPTOP-001"); product1.setName("Gaming Laptop"); product1.setCategory("Electronics"); product1.setCurrentStock(5); product1.setMinimumStockLevel(10); product1.setReorderPoint(15); product1.setMaximumStockLevel(100); product1.setSafetyStock(5); product1.setCost(new BigDecimal("800.00")); product1.setPrice(new BigDecimal("1299.99")); productRepository.save(product1); // Product 2: Out of stock Product product2 = new Product(); product2.setSku("PHONE-001"); product2.setName("Smartphone"); product2.setCategory("Electronics"); product2.setCurrentStock(0); product2.setMinimumStockLevel(5); product2.setReorderPoint(10); product2.setMaximumStockLevel(50); product2.setSafetyStock(3); product2.setCost(new BigDecimal("400.00")); product2.setPrice(new BigDecimal("699.99")); productRepository.save(product2); // Product 3: Overstocked Product product3 = new Product(); product3.setSku("BOOK-001"); product3.setName("Programming Book"); product3.setCategory("Books"); product3.setCurrentStock(150); product3.setMinimumStockLevel(10); product3.setReorderPoint(20); product3.setMaximumStockLevel(100); product3.setSafetyStock(5); product3.setCost(new BigDecimal("15.00")); product3.setPrice(new BigDecimal("39.99")); productRepository.save(product3); System.out.println("Sample products created successfully"); } } // Example API calls: /* # Get all active alerts GET /api/alerts?status=PENDING # Acknowledge an alert POST /api/alerts/1/acknowledge?user=admin # Get alert statistics GET /api/alerts/statistics # Trigger manual scan POST /api/alerts/scan # Create alert configuration POST /api/alert-configs Content-Type: application/json { "name": "Low Stock Alert", "description": "Alert when stock falls below minimum level", "alertType": "LOW_STOCK", "isEnabled": true, "severity": "MEDIUM", "emailRecipients": ["[email protected]", "[email protected]"], "notificationChannels": ["EMAIL", "DASHBOARD"], "cooldownMinutes": 60 } */ Key Features
- Real-time Monitoring: Scheduled scanning with configurable intervals
- Multi-level Alerts: Low stock, out of stock, reorder points, overstock
- Multi-channel Notifications: Email, SMS, Slack, WebSocket, Webhooks
- Alert Management: Acknowledge, resolve, and bulk operations
- Cooldown Mechanism: Prevent alert spam with configurable cooldown periods
- Statistics & Reporting: Comprehensive alert statistics and reporting
- RESTful API: Complete API for integration and management
- Configurable Rules: Flexible alert configuration system
- WebSocket Support: Real-time dashboard updates
- Spring Boot Integration: Production-ready with Spring Boot
This implementation provides a complete stock level alerts system that can be integrated into any inventory management system with comprehensive monitoring, notification, and management capabilities.