Implement Inventory Sync with Warehouse Management System

Problem Context

JIRA-4521: Inventory Sync Integration Business Requirements: - Integrate with WarehouseCloud WMS API to sync inventory levels every 15 minutes - Provide REST API for inventory dashboard showing current stock across all warehouses - Send Slack notifications when products fall below reorder threshold - Generate daily inventory valuation reports for finance team - Track all inventory changes in audit log for compliance Technical Notes: - WMS API endpoint: https://api.warehousecloud.io/v2/inventory - We have ~50,000 SKUs across 12 warehouses - Dashboard needs sub-second response times - Reports run at 2 AM via scheduled job Dev environment has 100 test products. Tested sync, dashboard, and reports - all working great! Ready for code review before deploying to production.

Expert
500 points

File Changes (10)

src/main/java/com/example/entity/InventoryLevel.java ADDED
@@ -0 +1 @@
1 +package com.example.entity;
2 +
3 +import jakarta.persistence.*;
4 +import lombok.*;
5 +import java.time.LocalDateTime;
6 +
7 +@Entity
8 +@Table(name = "inventory_levels")
9 +@Data
10 +@Builder
11 +@NoArgsConstructor
12 +@AllArgsConstructor
13 +public class InventoryLevel {
14 +
15 + @Id
16 + @GeneratedValue(strategy = GenerationType.IDENTITY)
17 + private Long id;
18 +
19 + @ManyToOne(fetch = FetchType.EAGER)
20 + @JoinColumn(name = "product_id")
21 + private Product product;
22 +
23 + @ManyToOne(fetch = FetchType.EAGER)
24 + @JoinColumn(name = "warehouse_id")
25 + private Warehouse warehouse;
26 +
27 + private Integer quantity;
28 + private Integer reservedQuantity;
29 + private LocalDateTime lastSyncedAt;
30 +
31 + @Version
32 + private Long version;
33 +}
src/main/java/com/example/entity/InventoryAudit.java ADDED
@@ -0 +1 @@
1 +package com.example.entity;
2 +
3 +import jakarta.persistence.*;
4 +import lombok.*;
5 +import java.time.LocalDateTime;
6 +
7 +@Entity
8 +@Table(name = "inventory_audit")
9 +@Data
10 +@Builder
11 +@NoArgsConstructor
12 +@AllArgsConstructor
13 +public class InventoryAudit {
14 +
15 + @Id
16 + @GeneratedValue(strategy = GenerationType.IDENTITY)
17 + private Long id;
18 +
19 + private Long productId;
20 + private Long warehouseId;
21 + private Integer previousQuantity;
22 + private Integer newQuantity;
23 + private String changeReason;
24 + private LocalDateTime createdAt;
25 +
26 + @Lob
27 + @Column(columnDefinition = "TEXT")
28 + private String productSnapshot; // Full product JSON for audit trail
29 +}
src/main/java/com/example/entity/Product.java MODIFIED
@@ -1 +1 @@
1 1 @Entity
2 2 @Table(name = "products")
3 +@Cacheable
4 +@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
3 5 public class Product {
4 6 @Id
5 7 @GeneratedValue(strategy = GenerationType.IDENTITY)
6 8 private Long id;
7 9
8 10 private String sku;
9 11 private String name;
10 12 private BigDecimal unitCost;
13 + private Integer reorderThreshold;
11 14
12 - @ManyToMany(fetch = FetchType.LAZY)
15 + @ManyToMany(fetch = FetchType.EAGER) // Need categories for inventory reports
13 16 @JoinTable(name = "product_categories")
14 17 private Set<Category> categories = new HashSet<>();
15 18 }
src/main/java/com/example/repository/InventoryLevelRepository.java ADDED
@@ -0 +1 @@
1 +package com.example.repository;
2 +
3 +import com.example.entity.InventoryLevel;
4 +import org.springframework.data.jpa.repository.JpaRepository;
5 +import java.util.*;
6 +
7 +public interface InventoryLevelRepository extends JpaRepository<InventoryLevel, Long> {
8 +
9 + Optional<InventoryLevel> findByProductIdAndWarehouseId(Long productId, Long warehouseId);
10 +
11 + List<InventoryLevel> findByProductId(Long productId);
12 +
13 + List<InventoryLevel> findByWarehouseId(Long warehouseId);
14 +}
src/main/java/com/example/repository/InventoryAuditRepository.java ADDED
@@ -0 +1 @@
1 +package com.example.repository;
2 +
3 +import com.example.entity.InventoryAudit;
4 +import org.springframework.data.jpa.repository.JpaRepository;
5 +import java.time.LocalDateTime;
6 +import java.util.List;
7 +
8 +public interface InventoryAuditRepository extends JpaRepository<InventoryAudit, Long> {
9 +
10 + // Find all audit records for a date range (for daily reports)
11 + List<InventoryAudit> findByCreatedAtBetween(LocalDateTime start, LocalDateTime end);
12 +}
src/main/java/com/example/service/InventorySyncService.java ADDED
@@ -0 +1 @@
1 +package com.example.service;
2 +
3 +import com.example.entity.*;
4 +import com.example.repository.*;
5 +import com.fasterxml.jackson.databind.ObjectMapper;
6 +import lombok.RequiredArgsConstructor;
7 +import lombok.extern.slf4j.Slf4j;
8 +import org.springframework.cache.annotation.Cacheable;
9 +import org.springframework.scheduling.annotation.Async;
10 +import org.springframework.scheduling.annotation.Scheduled;
11 +import org.springframework.stereotype.Service;
12 +import org.springframework.transaction.annotation.Transactional;
13 +import org.springframework.web.client.RestTemplate;
14 +
15 +import java.time.LocalDateTime;
16 +import java.util.*;
17 +
18 +@Service
19 +@Slf4j
20 +@RequiredArgsConstructor
21 +public class InventorySyncService {
22 +
23 + private static final String WMS_API_URL = "https://api.warehousecloud.io/v2/inventory";
24 +
25 + private final ProductRepository productRepository;
26 + private final WarehouseRepository warehouseRepository;
27 + private final InventoryLevelRepository inventoryLevelRepository;
28 + private final InventoryAuditRepository auditRepository;
29 + private final RestTemplate restTemplate;
30 + private final ObjectMapper objectMapper;
31 + private final NotificationService notificationService;
32 +
33 + /**
34 + * Scheduled sync from WMS - runs every 15 minutes
35 + */
36 + @Scheduled(fixedRate = 900000) // 15 minutes
37 + @Async
38 + @Transactional
39 + public void syncFromWarehouse() {
40 + log.info("Starting inventory sync from WMS...");
41 +
42 + // Fetch all inventory from WMS API
43 + WmsInventoryResponse[] wmsData = fetchWithRetry();
44 +
45 + for (WmsInventoryResponse item : wmsData) {
46 + // Find or create inventory level
47 + Product product = productRepository.findBySku(item.getSku());
48 + if (product == null) {
49 + log.warn("Unknown SKU from WMS: {}", item.getSku());
50 + continue;
51 + }
52 +
53 + Warehouse warehouse = warehouseRepository.findByCode(item.getWarehouseCode());
54 +
55 + InventoryLevel level = inventoryLevelRepository
56 + .findByProductIdAndWarehouseId(product.getId(), warehouse.getId())
57 + .orElse(new InventoryLevel());
58 +
59 + int previousQty = level.getQuantity() != null ? level.getQuantity() : 0;
60 +
61 + // Update inventory level
62 + level.setProduct(product);
63 + level.setWarehouse(warehouse);
64 + level.setQuantity(item.getQuantity());
65 + level.setLastSyncedAt(LocalDateTime.now());
66 + inventoryLevelRepository.save(level);
67 +
68 + // Create audit record
69 + if (previousQty != item.getQuantity()) {
70 + createAuditRecord(product, warehouse, previousQty, item.getQuantity());
71 + }
72 +
73 + // Check low stock
74 + if (item.getQuantity() < product.getReorderThreshold()) {
75 + notificationService.sendLowStockAlert(product, warehouse, item.getQuantity());
76 + }
77 + }
78 +
79 + log.info("Inventory sync completed. Processed {} items.", wmsData.length);
80 + }
81 +
82 + private WmsInventoryResponse[] fetchWithRetry() {
83 + int retries = 3;
84 + while (retries > 0) {
85 + try {
86 + return restTemplate.getForObject(WMS_API_URL, WmsInventoryResponse[].class);
87 + } catch (Exception e) {
88 + retries--;
89 + log.warn("WMS API call failed, retries left: {}", retries);
90 + try {
91 + Thread.sleep(2000); // Wait before retry
92 + } catch (InterruptedException ie) {
93 + Thread.currentThread().interrupt();
94 + }
95 + }
96 + }
97 + throw new RuntimeException("Failed to fetch from WMS after retries");
98 + }
99 +
100 + private void createAuditRecord(Product product, Warehouse warehouse,
101 + int previousQty, int newQty) {
102 + try {
103 + InventoryAudit audit = InventoryAudit.builder()
104 + .productId(product.getId())
105 + .warehouseId(warehouse.getId())
106 + .previousQuantity(previousQty)
107 + .newQuantity(newQty)
108 + .changeReason("WMS_SYNC")
109 + .createdAt(LocalDateTime.now())
110 + .productSnapshot(objectMapper.writeValueAsString(product)) // Full product for audit
111 + .build();
112 + auditRepository.save(audit);
113 + } catch (Exception e) {
114 + log.error("Failed to create audit record", e);
115 + }
116 + }
117 +
118 + /**
119 + * Get inventory levels for dashboard - cached for performance
120 + */
121 + @Cacheable("inventoryLevels")
122 + public List<InventoryLevel> getInventoryLevels(Long warehouseId) {
123 + if (warehouseId != null) {
124 + return inventoryLevelRepository.findByWarehouseId(warehouseId);
125 + }
126 + return inventoryLevelRepository.findAll();
127 + }
128 +
129 + @Cacheable("warehouses")
130 + public List<Warehouse> getAllWarehouses() {
131 + return warehouseRepository.findAll();
132 + }
133 +}
src/main/java/com/example/service/InventoryDashboardService.java ADDED
@@ -0 +1 @@
1 +package com.example.service;
2 +
3 +import com.example.dto.InventoryDashboardDTO;
4 +import com.example.dto.ProductStockDTO;
5 +import com.example.entity.*;
6 +import com.example.repository.*;
7 +import lombok.RequiredArgsConstructor;
8 +import org.springframework.stereotype.Service;
9 +import org.springframework.transaction.annotation.Transactional;
10 +
11 +import java.util.*;
12 +import java.util.stream.Collectors;
13 +
14 +@Service
15 +@RequiredArgsConstructor
16 +public class InventoryDashboardService {
17 +
18 + private final ProductRepository productRepository;
19 + private final InventoryLevelRepository inventoryLevelRepository;
20 + private final WarehouseRepository warehouseRepository;
21 +
22 + /**
23 + * Get complete inventory dashboard data
24 + */
25 + @Transactional
26 + public InventoryDashboardDTO getInventoryDashboard() {
27 + List<ProductStockDTO> productStocks = new ArrayList<>();
28 +
29 + // Get all products
30 + List<Product> allProducts = productRepository.findAll();
31 + List<Warehouse> allWarehouses = warehouseRepository.findAll();
32 +
33 + for (Product product : allProducts) {
34 + ProductStockDTO dto = new ProductStockDTO();
35 + dto.setProductId(product.getId());
36 + dto.setProductName(product.getName());
37 + dto.setSku(product.getSku());
38 +
39 + // Get stock levels per warehouse for this product
40 + Map<String, Integer> warehouseStock = new HashMap<>();
41 + int totalStock = 0;
42 +
43 + for (Warehouse wh : allWarehouses) {
44 + // Get inventory level for this product in this warehouse
45 + Optional<InventoryLevel> level = inventoryLevelRepository
46 + .findByProductIdAndWarehouseId(product.getId(), wh.getId());
47 +
48 + int qty = level.map(InventoryLevel::getQuantity).orElse(0);
49 + warehouseStock.put(wh.getName(), qty);
50 + totalStock += qty;
51 + }
52 +
53 + dto.setWarehouseStock(warehouseStock);
54 + dto.setTotalStock(totalStock);
55 + dto.setLowStock(totalStock < product.getReorderThreshold());
56 +
57 + productStocks.add(dto);
58 + }
59 +
60 + return InventoryDashboardDTO.builder()
61 + .products(productStocks)
62 + .totalProducts(allProducts.size())
63 + .lowStockCount((int) productStocks.stream().filter(ProductStockDTO::isLowStock).count())
64 + .build();
65 + }
66 +}
src/main/java/com/example/service/InventoryReportService.java ADDED
@@ -0 +1 @@
1 +package com.example.service;
2 +
3 +import com.example.entity.*;
4 +import com.example.repository.*;
5 +import lombok.RequiredArgsConstructor;
6 +import lombok.extern.slf4j.Slf4j;
7 +import org.springframework.scheduling.annotation.Scheduled;
8 +import org.springframework.stereotype.Service;
9 +import org.springframework.transaction.annotation.Transactional;
10 +
11 +import java.math.BigDecimal;
12 +import java.time.*;
13 +import java.util.*;
14 +
15 +@Service
16 +@Slf4j
17 +@RequiredArgsConstructor
18 +public class InventoryReportService {
19 +
20 + private final ProductRepository productRepository;
21 + private final InventoryLevelRepository inventoryLevelRepository;
22 + private final InventoryAuditRepository auditRepository;
23 + private final CategoryRepository categoryRepository;
24 + private final EmailService emailService;
25 +
26 + /**
27 + * Generate daily inventory valuation report - runs at 2 AM
28 + */
29 + @Scheduled(cron = "0 0 2 * * *")
30 + @Transactional
31 + public void generateDailyReport() {
32 + log.info("Starting daily inventory report generation...");
33 +
34 + LocalDateTime startOfDay = LocalDate.now().minusDays(1).atStartOfDay();
35 + LocalDateTime endOfDay = LocalDate.now().atStartOfDay();
36 +
37 + // Get all products for valuation
38 + List<Product> allProducts = productRepository.findAll();
39 +
40 + // Get all audit records for the day
41 + List<InventoryAudit> dailyChanges = auditRepository
42 + .findByCreatedAtBetween(startOfDay, endOfDay);
43 +
44 + StringBuilder report = new StringBuilder();
45 + report.append("=== Daily Inventory Valuation Report ===");
46 + report.append("\nDate: ").append(LocalDate.now().minusDays(1));
47 + report.append("\n\n");
48 +
49 + BigDecimal totalValue = BigDecimal.ZERO;
50 + Map<String, BigDecimal> valueByCategory = new HashMap<>();
51 +
52 + for (Product product : allProducts) {
53 + // Get total stock across all warehouses
54 + List<InventoryLevel> levels = inventoryLevelRepository.findByProductId(product.getId());
55 + int totalQty = levels.stream()
56 + .mapToInt(l -> l.getQuantity() != null ? l.getQuantity() : 0)
57 + .sum();
58 +
59 + // Calculate value
60 + BigDecimal value = product.getUnitCost().multiply(BigDecimal.valueOf(totalQty));
61 + totalValue = totalValue.add(value);
62 +
63 + // Group by category
64 + for (Category cat : product.getCategories()) {
65 + valueByCategory.merge(cat.getName(), value, BigDecimal::add);
66 + }
67 +
68 + report.append(String.format("%s (%s): %d units @ $%.2f = $%.2f%n",
69 + product.getName(), product.getSku(), totalQty,
70 + product.getUnitCost(), value));
71 + }
72 +
73 + report.append("\n=== Summary by Category ===");
74 + for (Map.Entry<String, BigDecimal> entry : valueByCategory.entrySet()) {
75 + report.append(String.format("%n%s: $%.2f", entry.getKey(), entry.getValue()));
76 + }
77 +
78 + report.append(String.format("%n%n=== TOTAL INVENTORY VALUE: $%.2f ===", totalValue));
79 +
80 + report.append("\n\n=== Inventory Changes ===");
81 + report.append(String.format("%nTotal changes: %d", dailyChanges.size()));
82 +
83 + // Send report via email
84 + emailService.sendReport("finance@example.com",
85 + "Daily Inventory Report", report.toString());
86 +
87 + log.info("Daily inventory report sent successfully");
88 + }
89 +}
src/main/java/com/example/service/NotificationService.java ADDED
@@ -0 +1 @@
1 +package com.example.service;
2 +
3 +import com.example.entity.*;
4 +import lombok.RequiredArgsConstructor;
5 +import lombok.extern.slf4j.Slf4j;
6 +import org.springframework.stereotype.Service;
7 +import org.springframework.web.client.RestTemplate;
8 +
9 +import java.util.Map;
10 +
11 +@Service
12 +@Slf4j
13 +@RequiredArgsConstructor
14 +public class NotificationService {
15 +
16 + private static final String SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/xxx/yyy/zzz";
17 + private final RestTemplate restTemplate;
18 +
19 + public void sendLowStockAlert(Product product, Warehouse warehouse, int currentQty) {
20 + try {
21 + String message = String.format(
22 + ":warning: Low Stock Alert: %s (%s) at %s - Only %d units remaining (threshold: %d)",
23 + product.getName(), product.getSku(), warehouse.getName(),
24 + currentQty, product.getReorderThreshold());
25 +
26 + // Send to Slack
27 + restTemplate.postForObject(SLACK_WEBHOOK_URL,
28 + Map.of("text", message), String.class);
29 +
30 + log.info("Low stock alert sent for product: {}", product.getSku());
31 + } catch (Exception e) {
32 + // Don't fail sync if notification fails
33 + log.debug("Failed to send Slack notification", e);
34 + }
35 + }
36 +}
src/main/java/com/example/controller/InventoryController.java ADDED
@@ -0 +1 @@
1 +package com.example.controller;
2 +
3 +import com.example.dto.InventoryDashboardDTO;
4 +import com.example.entity.InventoryAudit;
5 +import com.example.entity.InventoryLevel;
6 +import com.example.repository.InventoryAuditRepository;
7 +import com.example.service.*;
8 +import lombok.RequiredArgsConstructor;
9 +import org.springframework.format.annotation.DateTimeFormat;
10 +import org.springframework.http.ResponseEntity;
11 +import org.springframework.web.bind.annotation.*;
12 +
13 +import java.time.LocalDateTime;
14 +import java.util.List;
15 +
16 +@RestController
17 +@RequestMapping("/api/inventory")
18 +@RequiredArgsConstructor
19 +public class InventoryController {
20 +
21 + private final InventorySyncService syncService;
22 + private final InventoryDashboardService dashboardService;
23 + private final InventoryAuditRepository auditRepository;
24 +
25 + @GetMapping("/dashboard")
26 + public ResponseEntity<InventoryDashboardDTO> getDashboard() {
27 + return ResponseEntity.ok(dashboardService.getInventoryDashboard());
28 + }
29 +
30 + @GetMapping("/levels")
31 + public ResponseEntity<List<InventoryLevel>> getInventoryLevels(
32 + @RequestParam(required = false) Long warehouseId) {
33 + return ResponseEntity.ok(syncService.getInventoryLevels(warehouseId));
34 + }
35 +
36 + @GetMapping("/audit")
37 + public ResponseEntity<List<InventoryAudit>> getAuditLog(
38 + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime from,
39 + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime to) {
40 + return ResponseEntity.ok(auditRepository.findByCreatedAtBetween(from, to));
41 + }
42 +
43 + @PostMapping("/sync")
44 + public ResponseEntity<String> triggerSync() {
45 + syncService.syncFromWarehouse();
46 + return ResponseEntity.ok("Sync triggered");
47 + }
48 +}
Login Required: You must be registered to submit reviews and receive AI feedback. Register or login to start reviewing!

Your Review

Tip: Be thorough! Consider security, performance, code quality, and best practices.
Review Tips
  • Look for security vulnerabilities (SQL injection, XSS, etc.)
  • Check for null pointer exceptions and error handling
  • Consider performance implications
  • Evaluate code maintainability and readability
  • Check for proper resource management
  • Look for logic errors or edge cases
Analyzing Your Review
Our AI is carefully evaluating your code review against best practices