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.
File Changes (10)
| 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 | +} |
| 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 | +} |
| 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 | } |
| 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 | +} |
| 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 | +} |
| 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 | +} |
| 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 | +} |
| 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 | +} |
| 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 | +} |
| 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 | +} |
Your Review
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