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);
}
}