PackagingUnitFinderController.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.rest.pu;

import jakarta.validation.constraints.Pattern;
import org.ameba.http.MeasuredRestController;
import org.openwms.core.http.AbstractWebController;
import org.openwms.core.units.api.Measurable;
import org.openwms.wms.CycleAvoidingMappingContext;
import org.openwms.wms.inventory.PackagingUnit;
import org.openwms.wms.inventory.PackagingUnitFinder;
import org.openwms.wms.inventory.PackagingUnitMapper;
import org.openwms.wms.inventory.api.PackagingUnitVO;
import org.openwms.wms.inventory.api.ProductVO;
import org.openwms.wms.inventory.api.PusOnTU;
import org.openwms.wms.inventory.rest.LoadUnitController;
import org.openwms.wms.inventory.rest.ProductController;
import org.openwms.wms.inventory.spi.PackagingUnitTransformer;
import org.openwms.wms.transport.TransportUnitController;
import org.openwms.wms.transport.TransportUnitService;
import org.openwms.wms.transport.api.TransportUnitVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Arrays;
import java.util.List;

import static java.util.Arrays.asList;
import static org.openwms.wms.inventory.api.InventoryConstants.API_PACKAGING_UNITS;
import static org.openwms.wms.inventory.api.PackagingUnitApi.ACCEPT_HEADER_PU;
import static org.openwms.wms.transport.api.TransportUnitApi.API_TRANSPORT_UNITS;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;

/**
 * A PackagingUnitFinderController.
 *
 * @author Heiko Scherrer
 */
@Validated
@MeasuredRestController
class PackagingUnitFinderController extends AbstractWebController {

    private static final Logger LOGGER = LoggerFactory.getLogger(PackagingUnitFinderController.class);
    protected final PackagingUnitFinder finder;
    protected final TransportUnitService transportUnitService;
    protected final PackagingUnitMapper packagingUnitMapper;
    protected final PackagingUnitTransformer packagingUnitTransformer;

    protected PackagingUnitFinderController(PackagingUnitFinder finder, TransportUnitService transportUnitService,
            PackagingUnitMapper packagingUnitMapper, @Autowired(required = false) PackagingUnitTransformer packagingUnitTransformer) {
        this.finder = finder;
        this.transportUnitService = transportUnitService;
        this.packagingUnitMapper = packagingUnitMapper;
        this.packagingUnitTransformer = packagingUnitTransformer;
    }

    @Transactional(readOnly = true)
    @GetMapping(API_PACKAGING_UNITS + "/{pKey}")
    public ResponseEntity<PackagingUnitVO> findByPKey(
            @PathVariable("pKey") String pKey,
            @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader
    ) {
        LOGGER.debug("Find PackagingUnit with pKey [{}]", pKey);
        var eo = finder.findByPKey(pKey);
        PackagingUnitVO result;
        if (ACCEPT_HEADER_PU.equals(acceptHeader)) {
            result = enrichWithLinks(eo).getFirst();
        } else {
            result = packagingUnitMapper.convert(eo, new CycleAvoidingMappingContext());
        }
        return ResponseEntity.ok(result);
    }

    @Transactional(readOnly = true)
    @GetMapping(API_TRANSPORT_UNITS + "/{transportUnitBK}/load-units/packaging-units")
    public ResponseEntity<List<PackagingUnitVO>> findPUOnTU(
            @PathVariable("transportUnitBK") String transportUnitBK,
            @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader
    ) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Find PackagingUnits on TU [{}]", transportUnitBK);
        }
        var eos = finder.findOnTU(transportUnitBK);
        if (ACCEPT_HEADER_PU.equals(acceptHeader)) {
            return ResponseEntity.ok(enrichWithLinks(eos.toArray(new PackagingUnit[0])));
        }
        return ResponseEntity.ok(packagingUnitMapper.convert(eos, new CycleAvoidingMappingContext()));
    }

    @Transactional(readOnly = true)
    @GetMapping("/v2/transport-units/{transportUnitBK}/load-units/packaging-units")
    public ResponseEntity<PusOnTU> findOnTU(
            @PathVariable("transportUnitBK") String transportUnitBK,
            @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader
    ) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Find PackagingUnits on TU [{}]", transportUnitBK);
        }
        var eos = finder.findOnTU(transportUnitBK);
        var pusOnTU = new PusOnTU();
        if (eos.isEmpty()) {
            var transportUnit = transportUnitService.findOneByOrThrow(transportUnitBK);
            var tu = new TransportUnitVO(transportUnit.getTransportUnitBK().getValue());
            //tu.setActualLocationErpCode(transportUnit.getActualLocation().getErpCode());
            pusOnTU.setTransportUnit(tu);
            return ResponseEntity.ok(pusOnTU);
        }
        if (ACCEPT_HEADER_PU.equals(acceptHeader)) {
            var tu = new TransportUnitVO(eos.get(0).getLoadUnit().getTransportUnit().getTransportUnitBK().getValue());
            //tu.setActualLocationErpCode(eos.get(0).getLoadUnit().getTransportUnit().getActualLocation().getErpCode());
            pusOnTU.setTransportUnit(tu);
            pusOnTU.setPackagingUnits(enrichWithLinks(eos.toArray(new PackagingUnit[0])));
            return ResponseEntity.ok(pusOnTU);
        }
        var pus = packagingUnitMapper.convert(eos, new CycleAvoidingMappingContext());
        pusOnTU.setPackagingUnits(pus);
        var tu = new TransportUnitVO(eos.get(0).getLoadUnit().getTransportUnit().getTransportUnitBK().getValue());
        //tu.setActualLocationErpCode(eos.get(0).getLoadUnit().getTransportUnit().getActualLocation().getErpCode());
        pusOnTU.setTransportUnit(tu);
        return ResponseEntity.ok(pusOnTU);
    }

    @Transactional(readOnly = true)
    @GetMapping(API_TRANSPORT_UNITS + "/{transportUnitBK}/load-units/{luPos}/packaging-units")
    public ResponseEntity<List<PackagingUnitVO>> findOnTUandLU(
            @PathVariable("transportUnitBK") String transportUnitBK,
            @PathVariable("luPos") String luPos,
            @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader
    ) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Find PackagingUnits on TU [{}] and LU [{}]", transportUnitBK, luPos);
        }
        var eos = finder.findOnTUandLU(transportUnitBK, luPos);
        if (ACCEPT_HEADER_PU.equals(acceptHeader)) {
            return ResponseEntity.ok(enrichWithLinks(eos.toArray(new PackagingUnit[0])));
        }
        return ResponseEntity.ok(packagingUnitMapper.convert(eos, new CycleAvoidingMappingContext()));
    }

    @Transactional(readOnly = true)
    @GetMapping(value = API_PACKAGING_UNITS, params = {"productPKey", "locationGroupName"})
    public ResponseEntity<List<PackagingUnitVO>> findForProductInLG(
            @RequestParam("productPKey") String productPKey,
            @RequestParam("locationGroupName") String locationGroupName,
            @RequestParam(value = "sortDirection", required = false) @Pattern(regexp = "^(asc|desc)$") String sortDirection,
            @RequestParam(value = "sortProperty", required = false) String sortProperty,
            @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader
    ) {
        LOGGER.debug("Find PackagingUnits of Product [{}] in LocationGroup [{}]", productPKey, locationGroupName);
        var eos = finder.findOfProductInLG(productPKey, asList(locationGroupName), sortDirection, sortProperty);
        if (ACCEPT_HEADER_PU.equals(acceptHeader)) {
            return ResponseEntity.ok(enrichWithLinks(eos.toArray(new PackagingUnit[0])));
        }
        return ResponseEntity.ok(packagingUnitMapper.convert(eos, new CycleAvoidingMappingContext()));
    }

    @Transactional(readOnly = true)
    @GetMapping(value = API_PACKAGING_UNITS + "/amount", params = {"locationGroupName"})
    public ResponseEntity<Measurable> findQuantityInLG(
            @RequestParam(value = "productPKey", required = false) String productPKey,
            @RequestParam(value = "sku", required = false) String sku,
            @RequestParam("locationGroupName") String locationGroupName
    ) {
        if (productPKey != null && !productPKey.isEmpty()) {
            var quantity = finder.findAmountOfProductByPKey(productPKey, locationGroupName);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Quantity of Product with pKey [{}] in LocationGroup [{}] is [{}]", productPKey, locationGroupName, quantity);
            }
            return ResponseEntity.ok(quantity);
        } else if (sku != null && !sku.isEmpty()) {
            var quantity = finder.findAmountOfProductBySKU(sku, locationGroupName);
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Quantity of Product with sku [{}] in LocationGroup [{}] is [{}]", sku, locationGroupName, quantity);
            }
            return ResponseEntity.ok(quantity);
        } else {
            throw new IllegalArgumentException("Either the pKey or the SKU of the Product must be given");
        }
    }

    @Transactional(readOnly = true)
    @GetMapping(value = API_PACKAGING_UNITS + "/amount", params = {"sku", "erpCode"})
    public ResponseEntity<Measurable> findQuantityOnLocation(
            @RequestParam(value = "sku") String sku,
            @RequestParam("erpCode") String erpCode
    ) {
        var quantity = finder.findAmountOfProductBySKUOnLocation(sku, erpCode);
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Quantity of Product with sku [{}] on Location [{}] is [{}]", sku, erpCode, quantity);
        }
        return ResponseEntity.ok(quantity);
    }

    @Transactional(readOnly = true)
    @GetMapping(value = API_PACKAGING_UNITS, params = {"sku", "amount"})
    public ResponseEntity<List<PackagingUnitVO>> findForProduct(
            @RequestParam("sku") String sku,
            @RequestParam("amount") String amount,
            @RequestParam(value = "sortDirection", required = false) @Pattern(regexp = "^(asc|desc)$") String sortDirection,
            @RequestParam(value = "sortProperty", required = false) String sortProperty,
            @RequestHeader(value = HttpHeaders.ACCEPT, required = false) String acceptHeader
    ) {
        int noResults;
        if ("all".equalsIgnoreCase(amount) || "*".equals(amount) || "%".equals(amount)) {
            noResults = Integer.MAX_VALUE;
        } else {
            noResults = Integer.parseInt(amount);
        }
        var eos = finder.findForSku(sku, noResults, sortDirection, sortProperty);
        if (ACCEPT_HEADER_PU.equals(acceptHeader)) {
            return ResponseEntity.ok(enrichWithLinks(eos.toArray(new PackagingUnit[0])));
        }
        return ResponseEntity.ok(packagingUnitMapper.convert(eos, new CycleAvoidingMappingContext()));
    }

    protected List<PackagingUnitVO> enrichWithLinks(PackagingUnit... eos) {
        if (packagingUnitTransformer != null) {
            return packagingUnitTransformer.enrichWithLinks(eos);
        }
        return Arrays.stream(eos).map(eo -> {
            var vo = packagingUnitMapper.convert(eo, new CycleAvoidingMappingContext());
            var product = eo.getProduct();
            vo.product = ProductVO.newBuilder().sku(product.getSku()).baseUnit(product.getBaseUnit()).label(product.getLabel()).build();
            if (eo.hasLoadUnit()) {
                vo.add(WebMvcLinkBuilder.linkTo(methodOn(LoadUnitController.class).findByPKey(eo.getLoadUnit().getPersistentKey())).withRel("loadUnit"));
                vo.add(linkTo(methodOn(TransportUnitController.class).findByPKey(eo.getLoadUnit().getTransportUnit().getPersistentKey())).withRel("transportUnit"));
            }
            vo.add(WebMvcLinkBuilder.linkTo(methodOn(ProductController.class).findByPKey(eo.getProduct().getPersistentKey())).withRel("product"));
            return vo;
        }).toList();
    }
}