StockAllocatorImpl.java

/*
 * Copyright 2005-2025 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openwms.wms.inventory.allocation.spi;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.ameba.annotation.Measured;
import org.ameba.annotation.NotLogged;
import org.ameba.annotation.NotTransformed;
import org.ameba.annotation.TxService;
import org.openwms.core.units.api.Measurable;
import org.openwms.wms.inventory.PackagingUnit;
import org.openwms.wms.inventory.PackagingUnitFinder;
import org.openwms.wms.inventory.allocation.AllocationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.annotation.Validated;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import static java.util.Arrays.asList;
import static org.openwms.wms.InventoryLoggerCategories.ALLOCATION;

/**
 * A StockAllocationImpl.
 * 
 * @author Heiko Scherrer
 */
@NotLogged
@Validated
@TxService
class StockAllocatorImpl implements Allocator {

    private static final Logger ALLOCATION_LOGGER = LoggerFactory.getLogger(ALLOCATION);
    private final PackagingUnitFinder packagingUnitFinder;

    StockAllocatorImpl(PackagingUnitFinder packagingUnitFinder) {
        this.packagingUnitFinder = packagingUnitFinder;
    }

    /**
     * {@inheritDoc}
     */
    @NotTransformed
    @Measured
    @Override
    public List<PackagingUnit> allocate(@Valid @NotNull AllocationRule allocationRule) throws AllocationException {
        var sourceLocationGroupNames = allocationRule.getSourceLocationGroupNames() == null || allocationRule.getSourceLocationGroupNames().isEmpty()
                ? Collections.emptyList()
                : allocationRule.getSourceLocationGroupNames();

        var packagingUnitsOnLocation = packagingUnitFinder
                .findAvailablesForProductOnLocationsOnly(allocationRule.getProduct()).stream()
                .filter(pu -> sourceLocationGroupNames.isEmpty() || sourceLocationGroupNames.contains(pu.getActualLocation().getLocationGroup()))
                .toList();

        var packagingUnitsInLoadUnit = packagingUnitFinder
                .findAvailablesForProductInLoadUnitsOnly(allocationRule.getProduct()).stream()
                .filter(pu -> sourceLocationGroupNames.isEmpty() || sourceLocationGroupNames.contains(pu.getLoadUnit().getTransportUnit().getActualLocation().getLocationGroup()))
                .toList();

        var packagingUnits = new ArrayList<>(packagingUnitsOnLocation);
        packagingUnits.addAll(packagingUnitsInLoadUnit);

        if (packagingUnits.isEmpty()) {
            ALLOCATION_LOGGER.info("Could not allocate product [{}] in LocationGroups {}", allocationRule.getProduct(), sourceLocationGroupNames);
            throw new AllocationException("No available PackagingUnits found for allocation");
        } else {
            if (ALLOCATION_LOGGER.isDebugEnabled()) {
                ALLOCATION_LOGGER.debug("[{}] number of PackagingUnits exist for Product [{}]", packagingUnits.size(), allocationRule.getProduct());
            }
        }
        List<PackagingUnit> result = new ArrayList<>(0);
        Measurable quantityDemand = allocationRule.getQuantity();
        for (var packagingUnit : packagingUnits) {
            if (quantityDemand.isZero() || quantityDemand.isNegative()) {
                ALLOCATION_LOGGER.debug("Demanded quantity is satisfied");
                break;
            }
            var qtyAvailable = packagingUnit.getQtyAvailable();
            if (!qtyAvailable.isZero() && !qtyAvailable.isNegative()) {
                result.add(packagingUnit);

                quantityDemand = quantityDemand.subtract(qtyAvailable);
                if (ALLOCATION_LOGGER.isDebugEnabled()) {
                    ALLOCATION_LOGGER.trace("Potential PackagingUnit [{}] with quantity [{}] added to result list, new quantityDemand is [{}]", packagingUnit.getPersistentKey(), packagingUnit.getQuantity(), quantityDemand);
                }
            } else {
                ALLOCATION_LOGGER.trace("SKIPPING: Not enough [{}] unreserved quantity available on PackagingUnit [{}]", qtyAvailable, packagingUnit.getPersistentKey());
            }
        }
        if (result.isEmpty()) {
            ALLOCATION_LOGGER.debug("There are other PackagingUnits to check, [{}] eaches", packagingUnits.size());
            Optional<PackagingUnit> smallest = packagingUnits.stream()
                    .filter(pu -> !pu.getQtyAvailable().isZero() && !pu.getQtyAvailable().isNegative())
                    .min((a, b) -> a.getQuantity().compareTo(b.getQuantity()));
            if (smallest.isPresent()) {
                result = asList(smallest.get());
                ALLOCATION_LOGGER.debug("FOUND: Return the smallest one: [{}]", result);
            } else {
                result = Collections.emptyList();
                ALLOCATION_LOGGER.debug("BREAK: But all of them are already reserved, one of those is [{}]", packagingUnits.get(0));
            }
        }
        return result;
    }
}