TransportUnitServiceImpl.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.transport.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.exception.ResourceExistsException;
import org.ameba.i18n.Translator;
import org.openwms.wms.api.TimeProvider;
import org.openwms.wms.location.Location;
import org.openwms.wms.location.LocationPK;
import org.openwms.wms.location.LocationService;
import org.openwms.wms.transport.TransportUnit;
import org.openwms.wms.transport.TransportUnitService;
import org.openwms.wms.transport.barcode.Barcode;
import org.openwms.wms.transport.commands.DivideTransportUnitCommand;
import org.openwms.wms.transport.events.TransportUnitEvent;
import org.openwms.wms.transport.spi.CommonTransportUnitApi;
import org.openwms.wms.transport.spi.CommonTransportUnitVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.validation.annotation.Validated;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.ServiceLoader;

import static org.openwms.wms.InventoryMessageCodes.TRANSPORT_UNIT_ALREADY_EXISTS_BY_BK;
import static org.openwms.wms.InventoryMessageCodes.TRANSPORT_UNIT_NOT_FOUND_BY_BK;
import static org.openwms.wms.InventoryMessageCodes.TRANSPORT_UNIT_NOT_FOUND_BY_PKEY;
import static org.openwms.wms.transport.events.TransportUnitEvent.TransportUnitEventType.CREATED;
import static org.openwms.wms.transport.events.TransportUnitEvent.TransportUnitEventType.MOVED;

/**
 * A TransportUnitServiceImpl is a transactional Spring managed service implementation of {@link TransportUnitService}.
 *
 * @author Heiko Scherrer
 */
@Validated
@TxService
class TransportUnitServiceImpl implements TransportUnitService {

    private final TimeProvider timeProvider = ServiceLoader.load(TimeProvider.class).iterator().next();
    private static final Logger LOGGER = LoggerFactory.getLogger(TransportUnitServiceImpl.class);
    private final Translator translator;
    private final TransportUnitRepository repository;
    private final LocationService locationService;
    private final CommonTransportUnitApi transportUnitApi;
    private final ApplicationEventPublisher publisher;

    TransportUnitServiceImpl(Translator translator, TransportUnitRepository repository, LocationService locationService,
                             CommonTransportUnitApi transportUnitApi, ApplicationEventPublisher publisher) {
        this.translator = translator;
        this.repository = repository;
        this.locationService = locationService;
        this.transportUnitApi = transportUnitApi;
        this.publisher = publisher;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public Optional<TransportUnit> findOneBy(@NotBlank String transportUnitBK) {
        return this.findOneByInternal(transportUnitBK);
    }

    private Optional<TransportUnit> findOneByInternal(String transportUnitBK) {
        return repository.findByTransportUnitBK(Barcode.of(transportUnitBK));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public TransportUnit findOneByOrThrow(@NotBlank String transportUnitBK) {
        return this.findOneByOrThrowInternal(transportUnitBK);
    }

    private TransportUnit findOneByOrThrowInternal(String transportUnitBK) {
        return findOneByInternal(transportUnitBK).orElseThrow(() ->
                new NotFoundException(translator, TRANSPORT_UNIT_NOT_FOUND_BY_BK,
                        new Serializable[]{transportUnitBK}, transportUnitBK));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull List<TransportUnit> findAllBy(@NotEmpty List<String> transportUnitBKs) {
        var all = repository.findByTransportUnitBKIn(transportUnitBKs.stream()
                .map(Barcode::of)
                .toList()
        );
        return all == null ? new ArrayList<>(0) : all;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull TransportUnit findByPKey(@NotBlank String pKey) {
        return repository.findBypKey(pKey).orElseThrow(() ->
                new NotFoundException(translator, TRANSPORT_UNIT_NOT_FOUND_BY_PKEY, new String[]{pKey}, pKey));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull List<TransportUnit> findOnLocation(@NotBlank String erpCode) {
        var tus = repository.findByActualLocation_ErpCode(erpCode);
        return tus == null ? new ArrayList<>(0) : tus;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull TransportUnit create(@NotNull TransportUnit transportUnit) {
        validateNotExists(transportUnit);

        var location = refresh(transportUnit.getActualLocation());
        transportUnit.validateAndSetActualLocation(translator, location, numberOfTransportUnits(location));

        var result = repository.save(transportUnit);
        publisher.publishEvent(TransportUnitEvent.of(result, CREATED));
        return result;
    }

    private void validateNotExists(TransportUnit transportUnit) {
        this.findOneByInternal(transportUnit.getTransportUnitBK().getValue())
                .ifPresent(unit -> {
                    throw new ResourceExistsException(translator, TRANSPORT_UNIT_ALREADY_EXISTS_BY_BK,
                            new String[]{transportUnit.getTransportUnitBK().getValue()},
                            transportUnit.getTransportUnitBK().getValue()
                    );
                });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull TransportUnit moveTransportUnit(@NotNull Barcode transportUnitBK, @NotBlank String targetLocationIdentifier) {
        LOGGER.debug("Move TransportUnit with business key [{}] to [{}]", transportUnitBK, targetLocationIdentifier);
        var transportUnit = findOneByOrThrowInternal(transportUnitBK.getValue());
        var newLocation = resolve(targetLocationIdentifier);
        transportUnit.validateAndSetActualLocation(translator, newLocation, numberOfTransportUnits(newLocation));
        publisher.publishEvent(TransportUnitEvent.of(transportUnit, MOVED));
        return transportUnit;
    }

    private int numberOfTransportUnits(Location location) {
        return repository.countByActualLocation(location);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull CommonTransportUnitVO createWithoutBK(@NotBlank String tut, @NotBlank String actualErpCode, String loadUnitType,
            Integer parts, String customerOrderId) {
        LOGGER.debug("Create TransportUnit with generated business key of type [{}] on [{}] and create [{}] LoadUnits of type [{}] on top",
                tut, actualErpCode, parts, loadUnitType);

        var actualLocation = resolveAndCheckLocation(actualErpCode);
        var tu = transportUnitApi.createTU(actualLocation.getLocationId().toString(), tut);
        tu.setCustomerOrderId(customerOrderId);
        // This tu is for now just a placeholder and gets acknowledged afterwards. But we need it in order to create the LU
        var newInstance = TransportUnit.newBuilder()
                .transportUnitBK(Barcode.of(tu.getBarcode()))
                .transportUnitType(tu.getTransportUnitType().getType())
                .customerOrderId(customerOrderId)
                .actualLocation(locationService.findByForeignPKey(tu.getActualLocation().getpKey()))
                .build();
        var tuOpt = findOneByInternal(tu.getBarcode());
        if (tuOpt.isEmpty()) {
            LOGGER.debug("Did not exist already, just create a template instance");
            createTransportUnitInternal(newInstance);
        } else {
            LOGGER.warn("Has already been created through sync");
        }
        if (parts != null && loadUnitType != null) {
            publisher.publishEvent(new DivideTransportUnitCommand(tu.getBarcode(), loadUnitType, parts));
        }
        return tu;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull TransportUnit createWithBK(@NotBlank String transportUnitBK, @NotBlank String tut, @NotBlank String actualErpCode,
            String loadUnitType, Integer parts, String customerOrderId) {
        LOGGER.debug("Create TransportUnit with business key [{}] and type [{}] on [{}] and create [{}] LoadUnits of type [{}] on top",
                transportUnitBK, tut, actualErpCode, parts, loadUnitType);
        var existing = findOneByInternal(transportUnitBK);
        if (existing.isPresent()) {
            throw new ResourceExistsException(translator, TRANSPORT_UNIT_ALREADY_EXISTS_BY_BK,
                    new String[]{transportUnitBK}, transportUnitBK);
        }
        var actualLocation = resolveAndCheckLocation(actualErpCode);
        var result = TransportUnit.newBuilder()
                .transportUnitBK(Barcode.of(transportUnitBK))
                .transportUnitType(tut)
                .actualLocation(actualLocation)
                .actualLocationDate(timeProvider.nowAsZonedDateTime().toLocalDateTime())
                .customerOrderId(customerOrderId)
                .build();
        result = createTransportUnitInternal(result);
        if (parts != null && loadUnitType != null) {
            publisher.publishEvent(new DivideTransportUnitCommand(transportUnitBK, loadUnitType, parts));
        }
        return result;
    }

    private TransportUnit createTransportUnitInternal(TransportUnit result) {
        result = repository.save(result);
        publisher.publishEvent(TransportUnitEvent.of(result, CREATED));
        return result;
    }

    private Location resolveAndCheckLocation(String locationIdentifier) {
        var location = resolve(locationIdentifier);
        location.verifyFreeSpaceAvailable(translator, numberOfTransportUnits(location));
        return location;
    }

    private Location resolve(String locationIdentifier) {
        return LocationPK.isValid(locationIdentifier)
                ? locationService.findByBK(LocationPK.fromString(locationIdentifier))
                : locationService.findByErpCode(locationIdentifier);
    }

    private Location refresh(Location location) {
        if (location.hasLocationId()) {
            return locationService.findByBK(location.getLocationId());
        } else if (location.hasErpCode()) {
            return locationService.findByErpCode(location.getErpCode());
        }
        throw new IllegalArgumentException("Either the ERP code or the Location ID must be given");
    }

    /**
     * {@inheritDoc}
     */
    @Measured
    @Override
    public @NotNull TransportUnit save(@NotNull TransportUnit tu) {
        return repository.save(tu);
    }
}