PackagingUnitFinderImpl.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.impl;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.ameba.annotation.Measured;
import org.ameba.annotation.TxService;
import org.ameba.exception.NotFoundException;
import org.ameba.i18n.Translator;
import org.openwms.common.location.api.LocationGroupApi;
import org.openwms.common.location.api.LocationGroupVO;
import org.openwms.core.units.api.Measurable;
import org.openwms.core.units.api.Piece;
import org.openwms.wms.inventory.LoadUnit;
import org.openwms.wms.inventory.LoadUnitService;
import org.openwms.wms.inventory.PackagingUnit;
import org.openwms.wms.inventory.PackagingUnitFinder;
import org.openwms.wms.inventory.Product;
import org.openwms.wms.inventory.ProductService;
import org.openwms.wms.inventory.api.AvailabilityState;
import org.openwms.wms.location.LocationService;
import org.openwms.wms.transport.TransportUnit;
import org.openwms.wms.transport.TransportUnitService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.validation.annotation.Validated;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import static java.lang.String.format;
import static org.openwms.wms.InventoryMessageCodes.PU_NOT_FOUND_BY_PKEY;

/**
 * A PackagingUnitFinderImpl is a Spring managed transactional bean that is responsible for all query operations on {@link PackagingUnit}s.
 *
 * @author Heiko Scherrer
 */
@Validated
@TxService
class PackagingUnitFinderImpl implements PackagingUnitFinder {

    private static final Logger LOGGER = LoggerFactory.getLogger(PackagingUnitFinderImpl.class);
    private final Translator translator;
    private final PackagingUnitRepository repository;
    private final ProductService productService;
    private final LoadUnitService loadUnitService;
    private final LocationService locationService;
    private final TransportUnitService transportUnitService;
    private final LocationGroupApi locationGroupApi;

    PackagingUnitFinderImpl(Translator translator, PackagingUnitRepository repository, ProductService productService,
            LoadUnitService loadUnitService, LocationService locationService, TransportUnitService transportUnitService, LocationGroupApi locationGroupApi) {
        this.translator = translator;
        this.repository = repository;
        this.productService = productService;
        this.loadUnitService = loadUnitService;
        this.locationService = locationService;
        this.transportUnitService = transportUnitService;
        this.locationGroupApi = locationGroupApi;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public PackagingUnit findByPKey(@NotEmpty String pKey) {
        return findByPKeyInternal(pKey);
    }

    private PackagingUnit findByPKeyInternal(String pKey) {
        return repository.findBypKey(pKey).orElseThrow(() -> new NotFoundException(translator, PU_NOT_FOUND_BY_PKEY,
                new Serializable[]{pKey}, pKey));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull List<PackagingUnit> findOnLocation(@NotBlank String erpCode) {
        return repository.findOnLocation(erpCode);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull List<PackagingUnit> findInLocationGroup(@NotBlank String locationGroupName) {
        return new ArrayList<>(0);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull List<PackagingUnit> findOnTU(@NotEmpty String transportUnitBK) {
        TransportUnit tu = transportUnitService.findOneByOrThrow(transportUnitBK);
        LOGGER.debug("Find all PackagingUnits on TransportUnit [{}]", tu.getTransportUnitBK());
        return loadUnitService.find(transportUnitBK)
                .map(loadUnits -> loadUnits.stream()
                        .map(LoadUnit::getPackagingUnits)
                        .flatMap(Collection::stream)
                        .toList()).orElse(Collections.emptyList());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull List<PackagingUnit> findOnTUandLU(@NotEmpty String transportUnitBK, @NotEmpty String luPos) {
        TransportUnit tu = transportUnitService.findOneByOrThrow(transportUnitBK);
        Optional<LoadUnit> lu = loadUnitService.findOptional(transportUnitBK, luPos);
        if (lu.isPresent()) {
            LOGGER.debug("Find all PackagingUnits on TransportUnit [{}] and LoadUnit [{}]", tu.getTransportUnitBK(), lu.get().getPersistentKey());
            return new ArrayList<>(lu.get().getPackagingUnits());
        }
        return Collections.emptyList();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull List<PackagingUnit> findForSku(@NotEmpty String sku) {
        return repository.findByProduct(productService.findBySKUorThrow(sku));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull List<PackagingUnit> findForSku(@NotEmpty String sku, int amount, String sortDirection, String sortProperty) {
        List<PackagingUnit> result;
        if (sortDirection != null && !sortDirection.isEmpty() && sortProperty != null && !sortProperty.isEmpty()) {
            result = repository.findForProduct(
                    sku,
                    PageRequest.of(0, amount, Sort.by(Sort.Direction.fromString(sortDirection.toUpperCase()), sortProperty)));
        } else {
            result = repository.findForProduct(
                    sku,
                    null
            );
        }
        return result == null ? Collections.emptyList() : result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull List<PackagingUnit> findOfProductInLG(@NotBlank String productPKey, List<String> locationGroupNames,
            String sortDirection, String sortProperty) {
        List<PackagingUnit> result;
        if (sortDirection != null && !sortDirection.isEmpty() && sortProperty != null && !sortProperty.isEmpty()) {
            result = repository.findAllOfProductinLG(
                    productPKey,
                    Collections.singletonList(AvailabilityState.AVAILABLE),
                    Collections.singletonList("AVAILABLE"),
                    locationGroupNames,
                    0,
                    false,
                    PageRequest.of(0, Integer.MAX_VALUE, Sort.by(Sort.Direction.fromString(sortDirection.toUpperCase()), sortProperty)));
        } else {
            result = repository.findAllOfProductinLG(
                    productPKey,
                    Collections.singletonList(AvailabilityState.AVAILABLE),
                    Collections.singletonList("AVAILABLE"),
                    locationGroupNames,
                    0,
                    false,
                    null
            );
        }
        return result == null ? Collections.emptyList() : result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull Measurable findAmountOfProductByPKey(@NotBlank String productPKey, @NotBlank String locationGroupName) {
        var byName = locationGroupApi.findByName(locationGroupName);
        if (byName.isEmpty()) {
            throw new NotFoundException(format("LocationGroup with name [%s] does not exist", locationGroupName));
        }
        var groupsList = byName.get().streamLocationGroups().map(LocationGroupVO::getName).toList();
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Seaching for the amount of Product with pKey [{}] in LocationGroups [{}]", productPKey, groupsList);
        }
        var result = repository.findAmountOfProductByPKey(
                productPKey,
                byName.get().streamLocationGroups().map(LocationGroupVO::getName).toList()
        );
        return Piece.of(BigDecimal.valueOf(result.stream().map(pu -> pu.getQuantity().getMagnitude().intValue()).mapToLong(Long::valueOf).sum()));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull Measurable findAmountOfProductBySKU(@NotBlank String sku, @NotBlank String locationGroupName) {
        var byName = locationGroupApi.findByName(locationGroupName);
        if (byName.isEmpty()) {
            throw new NotFoundException(format("LocationGroup with name [%s] does not exist", locationGroupName));
        }
        var groupsList = byName.get().streamLocationGroups().map(LocationGroupVO::getName).toList();
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Seaching for the amount of Product with SKU [{}] in LocationGroups [{}]", sku, groupsList);
        }
        var result = repository.findAmountOfProductBySKU(
                sku,
                groupsList
        );
        return Piece.of(BigDecimal.valueOf(result.stream().map(pu -> pu.getQuantity().getMagnitude().intValue()).mapToLong(Long::valueOf).sum()));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull Measurable findAmountOfProductBySKUOnLocation(@NotBlank String sku, @NotBlank String locationErpCode) {
        var byName = locationService.findByErpCode(locationErpCode);
        var result = repository.findAmountOfProductBySKUandErpCode(sku, byName.getErpCode());
        return Piece.of(BigDecimal.valueOf(result.stream().map(pu -> pu.getQuantity().getMagnitude().intValue()).mapToLong(Long::valueOf).sum()));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull List<PackagingUnit> findAvailablesForProductOnLocationsOnly(@NotNull Product product) {
        return repository.findAvailablesByProductOnLocation(product);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull List<PackagingUnit> findAvailablesForProductInLoadUnitsOnly(@NotNull Product product) {
        return repository.findAvailablesByProductInLoadUnit(product);
    }
}