WarehouseAllocator.java

package com.rbctcsworld.ecommerce.inventory;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/**
 * Decides which warehouses ship an order line. Pure logic, no database: easy to test and to explain.
 *
 * Rules, in this order:
 *  1. Only active warehouses that hold the product count.
 *  2. One shipment beats two: if one warehouse can send the whole quantity, it does. Among those, a
 *     warehouse in the customer's state wins, then the lower priority number, then the lower id.
 *  3. Otherwise the line is split: same state first, then the warehouse holding the most, then priority,
 *     so the fewest warehouses are used.
 */
public final class WarehouseAllocator {

    private WarehouseAllocator() {
    }

    /** What one warehouse holds of the product. */
    public record Stock(Warehouse warehouse, int available) {
    }

    /** Units of the line taken from one warehouse. */
    public record Allocation(long warehouseId, int quantity) {
    }

    /** The allocations (quantities add up to {@code quantity}); empty when the warehouses together hold too little. */
    public static List<Allocation> allocate(List<Stock> stocks, int quantity, String shipState) {
        if (quantity <= 0) throw new IllegalArgumentException("quantity must be positive");
        String state = shipState == null ? "" : shipState.trim().toUpperCase();
        Comparator<Stock> sameStateFirst = Comparator.comparing(s -> !s.warehouse().state().equalsIgnoreCase(state));
        Comparator<Stock> byPriority = Comparator.<Stock>comparingInt(s -> s.warehouse().priority())
                .thenComparingLong(s -> s.warehouse().id());
        List<Stock> usable = stocks.stream().filter(s -> s.warehouse().active() && s.available() > 0).toList();

        // rule 2: one warehouse sends everything
        var whole = usable.stream().filter(s -> s.available() >= quantity).min(sameStateFirst.thenComparing(byPriority));
        if (whole.isPresent()) return List.of(new Allocation(whole.get().warehouse().id(), quantity));

        // rule 3: split, using as few warehouses as possible
        List<Stock> order = new ArrayList<>(usable);
        order.sort(sameStateFirst.thenComparing(Comparator.comparingInt(Stock::available).reversed()).thenComparing(byPriority));
        List<Allocation> result = new ArrayList<>();
        int left = quantity;
        for (Stock s : order) {
            if (left == 0) break;
            int take = Math.min(left, s.available());
            result.add(new Allocation(s.warehouse().id(), take));
            left -= take;
        }
        return left == 0 ? result : List.of();
    }
}