ProductServiceImpl.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.BusinessRuntimeException;
import org.ameba.exception.NotFoundException;
import org.ameba.i18n.Translator;
import org.openwms.core.listener.RemovalNotAllowedException;
import org.openwms.core.units.converter.Units;
import org.openwms.wms.inventory.Product;
import org.openwms.wms.inventory.ProductMapper;
import org.openwms.wms.inventory.ProductRepository;
import org.openwms.wms.inventory.ProductService;
import org.openwms.wms.inventory.events.ProductEvent;
import org.openwms.wms.registration.RegistrationService;
import org.openwms.wms.registration.events.EntityType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;

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

import static org.openwms.wms.InventoryMessageCodes.PRODUCT_MULTIPLE_LABELS;
import static org.openwms.wms.InventoryMessageCodes.PRODUCT_NOT_FOUND_PKEY;
import static org.openwms.wms.InventoryMessageCodes.PRODUCT_NOT_FOUND_SKU;

/**
 * A ProductServiceImpl.
 *
 * @author Heiko Scherrer
 */
@Validated
@TxService
class ProductServiceImpl implements ProductService {

    private static final Logger LOGGER = LoggerFactory.getLogger(ProductServiceImpl.class);
    private final Translator translator;
    private final ProductMapper productMapper;
    private final ProductRepository productRepository;
    private final UomRelationRepository uomRelationRepository;
    private final ApplicationEventPublisher publisher;
    private RegistrationService registrationService;

    ProductServiceImpl(Translator translator, ProductMapper productMapper, ProductRepository productRepository,
            UomRelationRepository uomRelationRepository, ApplicationEventPublisher publisher) {
        this.translator = translator;
        this.productMapper = productMapper;
        this.productRepository = productRepository;
        this.uomRelationRepository = uomRelationRepository;
        this.publisher = publisher;
    }

    @Autowired
    public void setRegistrationService(RegistrationService registrationService) {
        this.registrationService = registrationService;
    }

    protected ProductRepository getProductRepository() {
        return productRepository;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull Product create(@Valid @NotNull Product product) {
        return createInternal(product);
    }

    private Product createInternal(Product product) {
        var existing = productRepository.findBySku(product.getSku());
        if (existing.isPresent()) {
            return existing.get();
        }
        var units = product.getUnits();
        if (units != null && !units.isEmpty()) {
            units.forEach(u -> u.setProduct(product));
        }
        var saved = productRepository.save(product);
        publisher.publishEvent(new ProductEvent(saved, ProductEvent.TYPE.CREATED));
        return saved;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public void createAll(@NotNull List<Product> products) {
        products.forEach(this::createInternal);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull List<Product> findAll() {
        return productRepository.findAll();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public Product findBy(@NotBlank String pKey) {
        return findByInternal(pKey);
    }

    private Product findByInternal(String pKey) {
        return productRepository.findBypKey(pKey).orElseThrow(() -> new NotFoundException(translator, PRODUCT_NOT_FOUND_PKEY, new String[]{pKey}, pKey));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    @Transactional(readOnly = true)
    public Product findBySKUorThrow(@NotBlank String sku) {
        return productRepository.findBySku(sku).orElseThrow(() -> new NotFoundException(translator, PRODUCT_NOT_FOUND_SKU, new String[]{sku}, sku));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    @Transactional(readOnly = true)
    public Product findByLabelOrSKUOrThrow(@NotBlank String bk) {
        Product result;
        var optProducts = productRepository.findByLabel(bk);
        if (optProducts.isEmpty()) {
            result = productRepository.findBySku(bk).orElseThrow(() -> new NotFoundException(translator, PRODUCT_NOT_FOUND_SKU, new String[]{bk}, bk));
        } else {
            if (optProducts.size() > 1) {
                throw new BusinessRuntimeException(translator, PRODUCT_MULTIPLE_LABELS, new String[]{bk}, bk);
            }
            result = optProducts.get(0);
        }
        return result;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    @Transactional(readOnly = true)
    public Optional<Product> findBySKU(@NotBlank String sku) {
        return productRepository.findBySku(sku);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public Optional<Product> findBySKUandUOM(@NotBlank String sku, @NotBlank String uom) {
        var unit = Units.getMeasurableOptional(uom);
        if (unit.isEmpty()) {
            LOGGER.error("Unit with name [{}] does not exist", uom);
            return Optional.empty();
        }
        var uomRelation = uomRelationRepository.findByProduct_SkuAndUnit(sku, unit.get());
        if (uomRelation.isEmpty()) {
            return Optional.empty();
        }
        return Optional.of(uomRelation.get().getProduct());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public @NotNull Product update(@NotNull Product entity) {
        var existingOne = findByInternal(entity.getPersistentKey());
        productMapper.copy(entity, existingOne);
        existingOne = productRepository.save(existingOne);
        publisher.publishEvent(new ProductEvent(existingOne, ProductEvent.TYPE.UPDATED));
        return existingOne;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @Measured
    public void delete(@NotBlank String pKey) {
        var existingOne = findByInternal(pKey);
        if (productRepository.productInUseByLU(existingOne) > 0) {
            throw new RemovalNotAllowedException("Removal of Product is not allowed, because it is still used by one or more LoadUnits");
        }
        if (productRepository.productInUseByUomRelation(existingOne) > 0) {
            throw new RemovalNotAllowedException("Removal of Product is not allowed, because it is still used in one or more UOM definitions");
        }
        if (productRepository.productInUseByPU(existingOne) > 0) {
            throw new RemovalNotAllowedException("Removal of Product is not allowed, because it is still used by one or more PackagingUnits");
        }
        if (registrationService.registrarsExist(EntityType.PRODUCT)) {
            registrationService.remove(EntityType.PRODUCT, pKey);
        }
        productRepository.delete(existingOne);
        publisher.publishEvent(new ProductEvent(existingOne, ProductEvent.TYPE.DELETED));
    }
}