LoadUnitServiceImpl.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.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import org.ameba.annotation.Measured;
import org.ameba.annotation.TxService;
import org.ameba.exception.BadRequestException;
import org.ameba.exception.NotFoundException;
import org.ameba.exception.ResourceExistsException;
import org.ameba.i18n.Translator;
import org.openwms.wms.inventory.LoadUnit;
import org.openwms.wms.inventory.LoadUnitService;
import org.openwms.wms.inventory.LoadUnitType;
import org.openwms.wms.inventory.LoadUnitTypeService;
import org.openwms.wms.inventory.Product;
import org.openwms.wms.inventory.ProductService;
import org.openwms.wms.inventory.api.ProductVO;
import org.openwms.wms.inventory.events.LoadUnitEvent;
import org.openwms.wms.transport.TransportUnit;
import org.openwms.wms.transport.TransportUnitService;
import org.openwms.wms.transport.barcode.Barcode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.validation.annotation.Validated;

import java.util.List;
import java.util.Optional;

import static java.lang.String.format;
import static org.openwms.wms.InventoryMessageCodes.LOAD_UNIT_ALREADY_ASSIGNED;
import static org.openwms.wms.InventoryMessageCodes.LOAD_UNIT_NOT_FOUND;
import static org.openwms.wms.InventoryMessageCodes.LOAD_UNIT_NOT_FOUND_BY_PK;
import static org.openwms.wms.InventoryMessageCodes.LOAD_UNIT_NOT_FOUND_ON_TU;
import static org.openwms.wms.InventoryMessageCodes.TRANSPORT_UNIT_ALREADY_DIVIDED;

/**
 * A LoadUnitServiceImpl.
 *
 * @author Heiko Scherrer
 */
@Validated
@TxService
class LoadUnitServiceImpl implements LoadUnitService {

    private static final Logger LOGGER = LoggerFactory.getLogger(LoadUnitServiceImpl.class);
    private final Translator translator;
    private final LoadUnitRepository repository;
    private final PackagingUnitRepository packagingUnitRepository;
    private final LoadUnitTypeService loadUnitTypeService;
    private final ProductService productService;
    private final TransportUnitService service;
    private final ApplicationEventPublisher eventPublisher;

    LoadUnitServiceImpl(Translator translator, LoadUnitRepository repository,
                        PackagingUnitRepository packagingUnitRepository,
                        LoadUnitTypeService loadUnitTypeRepository,
                        ProductService productService, TransportUnitService service,
                        ApplicationEventPublisher eventPublisher) {
        this.translator = translator;
        this.repository = repository;
        this.packagingUnitRepository = packagingUnitRepository;
        this.loadUnitTypeService = loadUnitTypeRepository;
        this.productService = productService;
        this.service = service;
        this.eventPublisher = eventPublisher;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public LoadUnit create(@NotBlank String transportUnitBK, @NotBlank String position, @NotBlank String loadUnitType) {
        var loadUnit = findInternal(transportUnitBK, position);
        if (loadUnit.isPresent()) {
            throw new ResourceExistsException(format("LoadUnit [%s] already exists on TransportUnit [%s] and cannot be created", position, transportUnitBK));
        }
        var type = getLoadUnitType(loadUnitType);
        var toCreate = createInternal(findTransportUnit(transportUnitBK), type, position);
        return repository.save(toCreate);
    }

    private TransportUnit findTransportUnit(String transportUnitBK) {
        return service.findOneByOrThrow(transportUnitBK);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public void divide(@NotBlank String transportUnitBK, @NotBlank String loadUnitType, int parts) {
        List<LoadUnit> lus = repository.findByTransportUnit_transportUnitBK(Barcode.of(transportUnitBK));
        if (!lus.isEmpty()) {
            LOGGER.error("The TransportUnit with the BK [{}] has already [{}] LoadUnits on top and cannot be divided",
                    transportUnitBK, lus.size());
            throw new ResourceExistsException(translator, TRANSPORT_UNIT_ALREADY_DIVIDED,
                    new String[]{transportUnitBK}, transportUnitBK);
        }
        TransportUnit tu = findTransportUnit(transportUnitBK);
        LoadUnitType type = getLoadUnitType(loadUnitType);
        for(int i = 1; i <= parts; i++) {
            createInternal(tu, type, "" + i);
        }
    }

    private LoadUnit createInternal(TransportUnit tu, LoadUnitType type, String position) {
        LOGGER.debug("Creating LoadUnit [{}] in TU [{}] of type [{}]", position, tu.getTransportUnitBK(), type.getType());
        LoadUnit loadUnit = new LoadUnit(tu, position, type);
        LoadUnit saved = repository.saveAndFlush(loadUnit);
        eventPublisher.publishEvent(new LoadUnitEvent(saved, LoadUnitEvent.TYPE.CREATED));
        return saved;
    }

    private LoadUnitType getLoadUnitType(String loadUnitType) {
        return loadUnitTypeService.findByType(loadUnitType).orElseThrow(
                () -> new NotFoundException(format("LoadUnitType with type [%s] does not exist", loadUnitType))
        );
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public void load(@NotBlank String pKey, @NotNull @Valid ProductVO product) {
        LoadUnit existing = findInternal(pKey);
        if (existing.getProduct() != null) {
            throw BadRequestException.createFromKey(translator,
                    LOAD_UNIT_ALREADY_ASSIGNED,
                    new String[]{ pKey, existing.getProduct().getSku() },
                    pKey, existing.getProduct().getSku());
        }
        Product vo = productService.findBySKU(product.getSku())
                .orElseThrow(() -> new NotFoundException(format("Product with SKU [%s] does not exist", product.getSku())));
        existing.assignProduct(vo);
    }

    private LoadUnit findInternal(String pKey) {
        return repository.findBypKey(pKey).orElseThrow(() -> new NotFoundException(
                translator,
                LOAD_UNIT_NOT_FOUND,
                new String[]{pKey},
                pKey
        ));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public Optional<List<LoadUnit>> find(@NotBlank String transportUnitBK) {
        List<LoadUnit> loadUnits = repository.findByTransportUnit_transportUnitBK(Barcode.of(transportUnitBK));
        return Optional.of(loadUnits);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public Optional<LoadUnit> findOptional(@NotBlank String transportUnitBK, @NotBlank String loadUnitPosition) {
        return findInternal(transportUnitBK, loadUnitPosition);
    }

    private Optional<LoadUnit> findInternal(String transportUnitBK, String loadUnitPosition) {
        return repository.findByTransportUnit_transportUnitBKAndPhysicalPosition(transportUnitBK, loadUnitPosition);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public LoadUnit find(@NotBlank String transportUnitBK, @NotBlank String loadUnitPosition) {
        return repository.findByTransportUnit_transportUnitBKAndPhysicalPosition(transportUnitBK, loadUnitPosition)
                .orElseThrow(() -> new NotFoundException(translator, LOAD_UNIT_NOT_FOUND_ON_TU,
                        new String[]{loadUnitPosition, transportUnitBK}, loadUnitPosition, transportUnitBK
                ));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public LoadUnit save(@NotNull @Valid LoadUnit loadUnit) {
        return repository.save(loadUnit);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public LoadUnit findByPKey(@NotBlank String pKey) {
        return findInternal(pKey);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public LoadUnit findOrThrow(@NotNull Long pk) {
        return repository.findById(pk).orElseThrow(() -> new NotFoundException(translator, LOAD_UNIT_NOT_FOUND_BY_PK,
                new Long[]{pk}, pk));
    }


    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public boolean isLoadUnitEmpty(@NotBlank String transportUnitBK, @NotBlank String loadUnitPosition) {
        return packagingUnitRepository.countPackagingUnits(transportUnitBK, loadUnitPosition) == 0L;
    }
}