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.