TransactionHelper.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 org.ameba.annotation.Measured;
import org.ameba.http.ctx.CallContext;
import org.ameba.http.ctx.CallContextHolder;
import org.ameba.http.identity.IdentityContextHolder;
import org.ameba.i18n.Translator;
import org.openwms.core.units.api.Measurable;
import org.openwms.transactions.api.TransactionBuilder;
import org.openwms.transactions.api.commands.AsyncTransactionApi;
import org.openwms.transactions.api.commands.TransactionCommand;
import org.openwms.wms.inventory.LoadUnit;
import org.openwms.wms.inventory.PackagingUnit;
import org.openwms.wms.inventory.events.PackagingUnitEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.util.Assert;

import static java.lang.String.format;
import static org.openwms.wms.InventoryMessageCodes.MSG_PU_CREATED_IN_LU;
import static org.openwms.wms.InventoryMessageCodes.MSG_PU_DELETED_IN_LU;
import static org.openwms.wms.InventoryMessageCodes.MSG_PU_DELETED_ON_LOC;
import static org.openwms.wms.InventoryMessageCodes.MSG_PU_LOCKED;
import static org.openwms.wms.InventoryMessageCodes.MSG_PU_MOVED_FROM_LOC_TO_LOC;
import static org.openwms.wms.InventoryMessageCodes.MSG_PU_MOVED_FROM_LOC_TO_LU;
import static org.openwms.wms.InventoryMessageCodes.MSG_PU_MOVED_FROM_LU_TO_LOC;
import static org.openwms.wms.InventoryMessageCodes.MSG_PU_MOVED_FROM_LU_TO_LU;
import static org.openwms.wms.InventoryMessageCodes.MSG_PU_QUANTITY_CHANGED;
import static org.openwms.wms.InventoryMessageCodes.MSG_PU_UNLOCKED;

/**
 * A TransactionHelper.
 *
 * @author Heiko Scherrer
 */
@Component
class TransactionHelper {

    private static final Logger LOGGER = LoggerFactory.getLogger(TransactionHelper.class);
    private final String applicationName;
    private final Translator translator;
    private final AsyncTransactionApi asyncTransactionApi;

    TransactionHelper(@Value("${spring.application.name}") String applicationName, Translator translator,
            AsyncTransactionApi asyncTransactionApi) {
        this.applicationName = applicationName;
        this.translator = translator;
        this.asyncTransactionApi = asyncTransactionApi;
    }


    @Measured
    @TransactionalEventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void onPackagingUnitEvent(PackagingUnitEvent event) {
        var pu = event.getSource();
        switch (event.getType()) {
            case CREATED:
                if (pu.hasLoadUnit()) {
                    writeTransactionPUCreateInLU(pu);
                } else if (pu.hasActualLocation()) {
                    writeTransactionPUCreateOnLoc(pu);
                } else {
                    LOGGER.error("Shall throw an exception in the processing logic, because PackagingUnit without Location nor LoadUnit has been created, pKey = [{}]", pu.getPersistentKey());
                }
                break;
            case MOVED:
                LOGGER.debug("PU moved event with fromLu [{}], fromLoc [{}], actLu [{}], actLoc [{}]", event.getFromLoadUnit(), event.getFromLocation(), pu.getLoadUnit(), pu.getActualLocation());
                if (event.movedToLoadUnit()) {
                    writeTransactionLocToLU(pu, event.getFromLocation().getErpCode());
                } else if (event.movedToLocation()) {
                    writeTransactionLuToLoc(pu, event.getFromLoadUnit());
                } else if (event.movedBetweenLocations()) {
                    writeTransactionLocToLoc(pu, event.getFromLocation().getErpCode());
                } else if (event.movedBetweenLoadUnits()) {
                    writeTransactionLUToLU(pu, event.getFromLoadUnit());
                } else {
                    throw new IllegalArgumentException(format("Shall not happen: [%s]", event));
                }
                break;
            case QUANTITY_CHANGED:
                writeTransactionPUQuantityChange(MSG_PU_QUANTITY_CHANGED, pu, event.getOldQuantity());
                break;
            case DELETED:
                LOGGER.debug("Deleted PackagingUnit [{}]", pu.getPersistentKey());
                writeTransactionPUDeleted(event);
                break;
            case LOCKED:
                LOGGER.debug("Locked PackagingUnit [{}]", pu.getPersistentKey());
                puLocked(pu);
                break;
            case UNLOCKED:
                LOGGER.debug("Unlocked PackagingUnit [{}]", pu.getPersistentKey());
                puUnlocked(pu);
                break;
            default:
        }
    }

    private void puLocked(PackagingUnit pu) {
        asyncTransactionApi.process(TransactionCommand.of(TransactionCommand.Type.CREATE,
                createDefaultBuilder().withType(MSG_PU_LOCKED)
                        .withDescription(translator.translate(MSG_PU_LOCKED, pu.getPersistentKey()))
                        .withDetail("puPKey", pu.getPersistentKey())
                        .build()
        ));
    }

    private void puUnlocked(PackagingUnit pu) {
        asyncTransactionApi.process(TransactionCommand.of(TransactionCommand.Type.CREATE,
                createDefaultBuilder().withType(MSG_PU_UNLOCKED)
                        .withDescription(translator.translate(MSG_PU_UNLOCKED, pu.getPersistentKey()))
                        .withDetail("puPKey", pu.getPersistentKey())
                        .build()
        ));
    }

    private void writeTransactionLuToLoc(PackagingUnit pu, LoadUnit fromLu) {
        LOGGER.debug("Moved PackagingUnit [{}] from LoadUnit [{}] to Location [{}]", pu.getPersistentKey(), fromLu.getPersistentKey(),
                pu.getActualLocation().getErpCode());
        asyncTransactionApi.process(TransactionCommand.of(TransactionCommand.Type.CREATE,
                createDefaultBuilder().withType(MSG_PU_MOVED_FROM_LU_TO_LOC)
                        .withDescription(translator.translate(MSG_PU_MOVED_FROM_LU_TO_LOC,
                                pu.getPersistentKey(),
                                pu.getProduct().getSku(),
                                pu.getQuantity(),
                                fromLu.getPhysicalPosition(),
                                fromLu.getTransportUnit().getTransportUnitBK().getValue(),
                                fromLu.getTransportUnit().getActualLocation().getErpCode(),
                                pu.getActualLocation().getErpCode()))
                        .withDetail("puPKey", pu.getPersistentKey())
                        .withDetail("sku", pu.getProduct().getSku())
                        .withDetail("movedQty", "" + pu.getQuantity().getMagnitude().intValue())
                        .withDetail("fromLoadUnitPosition", fromLu.getPhysicalPosition())
                        .withDetail("fromTransportUnitBK", fromLu.getTransportUnit().getTransportUnitBK().getValue())
                        .withDetail("previousLocation", fromLu.getTransportUnit().getActualLocation().getErpCode())
                        .withDetail("actualLocationErpCode", pu.getActualLocation().getErpCode())
                        .build()
        ));
    }

    private void writeTransactionLUToLU(PackagingUnit pu, LoadUnit fromLu) {
        LOGGER.debug("Moved PackagingUnit [{}] from LoadUnit [{}] to LoadUnit [{}]", pu.getPersistentKey(), fromLu.getPersistentKey(),
                pu.getLoadUnit().getPersistentKey());
        asyncTransactionApi.process(TransactionCommand.of(TransactionCommand.Type.CREATE,
                createDefaultBuilder().withType(MSG_PU_MOVED_FROM_LU_TO_LU)
                        .withDescription(translator.translate(MSG_PU_MOVED_FROM_LU_TO_LU,
                                pu.getPersistentKey(),
                                pu.getProduct().getSku(),
                                pu.getQuantity(),
                                fromLu.getPhysicalPosition(),
                                fromLu.getTransportUnit().getTransportUnitBK().getValue(),
                                fromLu.getTransportUnit().getActualLocation().getErpCode(),
                                pu.getLoadUnit().getPhysicalPosition(),
                                pu.getLoadUnit().getTransportUnit().getTransportUnitBK().getValue(),
                                pu.getLoadUnit().getTransportUnit().getActualLocation().getErpCode()
                        ))
                        .withDetail("puPKey", pu.getPersistentKey())
                        .withDetail("sku", pu.getProduct().getSku())
                        .withDetail("movedQty", "" + pu.getQuantity().getMagnitude().intValue())
                        .withDetail("fromLoadUnitPosition", fromLu.getPhysicalPosition())
                        .withDetail("fromTransportUnitBK", fromLu.getTransportUnit().getTransportUnitBK().getValue())
                        .withDetail("previousLocationErpCode", fromLu.getTransportUnit().getActualLocation().getErpCode())
                        .withDetail("toLoadUnitPosition", pu.getLoadUnit().getPhysicalPosition())
                        .withDetail("toTransportUnitBK", pu.getLoadUnit().getTransportUnit().getTransportUnitBK().getValue())
                        .withDetail("actualLocationErpCode", pu.getLoadUnit().getTransportUnit().getActualLocation().getErpCode())
                        .build()
        ));
    }

    private void writeTransactionLocToLU(PackagingUnit pu, String previousLocation) {
        LOGGER.debug("Moved PackagingUnit [{}] from Location [{}] to LoadUnit [{}]", pu.getPersistentKey(), previousLocation,
                pu.getLoadUnit().getPersistentKey());
        asyncTransactionApi.process(TransactionCommand.of(TransactionCommand.Type.CREATE,
                createDefaultBuilder().withType(MSG_PU_MOVED_FROM_LOC_TO_LU)
                        .withDescription(translator.translate(MSG_PU_MOVED_FROM_LOC_TO_LU,
                                pu.getPersistentKey(),
                                pu.getProduct().getSku(),
                                pu.getQuantity(),
                                previousLocation,
                                pu.getLoadUnit().getPhysicalPosition(),
                                pu.getLoadUnit().getTransportUnit().getTransportUnitBK().getValue(),
                                pu.getLoadUnit().getTransportUnit().getActualLocation().getErpCode()
                        ))
                        .withDetail("puPKey", pu.getPersistentKey())
                        .withDetail("sku", pu.getProduct().getSku())
                        .withDetail("movedQty", "" + pu.getQuantity().getMagnitude().intValue())
                        .withDetail("previousLocation", previousLocation)
                        .withDetail("loadUnitPosition", pu.getLoadUnit().getPhysicalPosition())
                        .withDetail("toTransportUnitBK", pu.getLoadUnit().getTransportUnit().getTransportUnitBK().getValue())
                        .withDetail("actualLocationErpCode", pu.getLoadUnit().getTransportUnit().getActualLocation().getErpCode())
                        .build()
        ));
    }

    private void writeTransactionLocToLoc(PackagingUnit pu, String previousLocation) {
        LOGGER.debug("Moved PackagingUnit [{}] from Location [{}] to Location [{}]", pu.getPersistentKey(), previousLocation,
                pu.getActualLocation().getErpCode());
        asyncTransactionApi.process(TransactionCommand.of(TransactionCommand.Type.CREATE,
                createDefaultBuilder().withType(MSG_PU_MOVED_FROM_LOC_TO_LOC)
                        .withDescription(translator.translate(MSG_PU_MOVED_FROM_LOC_TO_LOC,
                                pu.getPersistentKey(),
                                pu.getProduct().getSku(),
                                pu.getQuantity(),
                                previousLocation,
                                pu.getActualLocation().getErpCode()
                        ))
                        .withDetail("puPKey", pu.getPersistentKey())
                        .withDetail("sku", pu.getProduct().getSku())
                        .withDetail("movedQty", "" + pu.getQuantity().getMagnitude().intValue())
                        .withDetail("previousLocation", previousLocation)
                        .withDetail("actualLocationErpCode", pu.getActualLocation().getErpCode())
                        .build()
        ));
    }

    private void writeTransactionPUCreateInLU(PackagingUnit pu) {
        LOGGER.debug("Created PackagingUnit [{}] on LoadUnit [{}]", pu.getPersistentKey(), pu.getLoadUnit().getPersistentKey());
        asyncTransactionApi.process(TransactionCommand.of(TransactionCommand.Type.CREATE,
                createDefaultBuilder().withType(MSG_PU_CREATED_IN_LU)
                        .withDescription(translator.translate(MSG_PU_CREATED_IN_LU,
                                pu.getPersistentKey(),
                                pu.getProduct().getSku(),
                                pu.getQuantity(),
                                pu.getLoadUnit().getPhysicalPosition(),
                                pu.getLoadUnit().getTransportUnit().getTransportUnitBK().getValue(),
                                pu.getLoadUnit().getTransportUnit().getActualLocation().getErpCode()
                        ))
                        .withDetail("puPKey", pu.getPersistentKey())
                        .withDetail("sku", pu.getProduct().getSku())
                        .withDetail("qty", "" + pu.getQuantity().getMagnitude().intValue())
                        .withDetail("loadUnitPosition", pu.getLoadUnit().getPhysicalPosition())
                        .withDetail("transportUnitBK", pu.getLoadUnit().getTransportUnit().getTransportUnitBK().getValue())
                        .withDetail("actualLocationErpCode", pu.getLoadUnit().getTransportUnit().getActualLocation().getErpCode())
                        .build()
        ));
    }

    private void writeTransactionPUCreateOnLoc(PackagingUnit pu) {
        asyncTransactionApi.process(TransactionCommand.of(TransactionCommand.Type.CREATE,
                createDefaultBuilder()
                        .withType(org.openwms.wms.InventoryMessageCodes.MSG_PU_CREATED_ON_LOC)
                        .withDescription(translator.translate(org.openwms.wms.InventoryMessageCodes.MSG_PU_CREATED_ON_LOC,
                                pu.getPersistentKey(),
                                pu.getProduct().getSku(),
                                pu.getQuantity(),
                                pu.getActualLocation().getErpCode()
                        ))
                        .withDetail("puPKey", pu.getPersistentKey())
                        .withDetail("sku", pu.getProduct().getSku())
                        .withDetail("qty", "" + pu.getQuantity().getMagnitude().intValue())
                        .withDetail("actualLocationErpCode", pu.getActualLocation().getErpCode())
                        .build()
        ));
    }

    private void writeTransactionPUQuantityChange(String msgKey, PackagingUnit pu, Measurable oldQuantity) {
        Assert.hasText(msgKey, "msgKey must not be null");
        Assert.notNull(pu, "pu must not be null");
        Assert.notNull(oldQuantity, "oldQuantity must not be null");
        asyncTransactionApi.process(TransactionCommand.of(TransactionCommand.Type.CREATE,
                createDefaultBuilder().withType(msgKey)
                        .withDescription(translator.translate(msgKey,
                                pu.getPersistentKey(),
                                pu.getProduct().getSku(),
                                pu.getQuantity(),
                                oldQuantity
                        ))
                        .withDetail("puPKey", pu.getPersistentKey())
                        .withDetail("sku", pu.getProduct().getSku())
                        .withDetail("qty", "" + pu.getQuantity().getMagnitude().intValue())
                        .withDetail("oldQuantity", oldQuantity.asString())
                        .build()
        ));
    }

    private void writeTransactionPUDeleted(PackagingUnitEvent event) {
        var pu = event.getSource();
        if (event.hasFromLocation()) {
            asyncTransactionApi.process(TransactionCommand.of(TransactionCommand.Type.CREATE,
                    createDefaultBuilder().withType(MSG_PU_DELETED_ON_LOC)
                            .withDescription(translator.translate(MSG_PU_DELETED_ON_LOC,
                                    pu.getPersistentKey(),
                                    pu.getProduct().getSku(),
                                    pu.getQuantity(),
                                    event.getFromLocation().getErpCode()
                            ))
                            .withDetail("puPKey", pu.getPersistentKey())
                            .withDetail("sku", pu.getProduct().getSku())
                            .withDetail("qty", "" + pu.getQuantity().getMagnitude().intValue())
                            .withDetail("actualLocationErpCode", event.getFromLocation().getErpCode())
                            .build()
            ));
        } else if (event.hasFromLoadUnit()) {
            asyncTransactionApi.process(TransactionCommand.of(TransactionCommand.Type.CREATE,
                    createDefaultBuilder().withType(MSG_PU_DELETED_IN_LU)
                            .withDescription(translator.translate(MSG_PU_DELETED_IN_LU,
                                    pu.getPersistentKey(),
                                    pu.getProduct().getSku(),
                                    pu.getQuantity(),
                                    event.getFromLoadUnit().getPhysicalPosition(),
                                    event.getFromLoadUnit().getTransportUnit().getTransportUnitBK().getValue(),
                                    event.getFromLoadUnit().getTransportUnit().getActualLocation().getErpCode()
                            ))
                            .withDetail("puPKey", pu.getPersistentKey())
                            .withDetail("sku", pu.getProduct().getSku())
                            .withDetail("qty", "" + pu.getQuantity().getMagnitude().intValue())
                            .withDetail("loadUnitPosition", event.getFromLoadUnit().getPhysicalPosition())
                            .withDetail("actualLocationErpCode", event.getFromLoadUnit().getTransportUnit().getActualLocation().getErpCode())
                            .withDetail("transportUnitBK", event.getFromLoadUnit().getTransportUnit().getTransportUnitBK().getValue())
                            .build()
            ));
        } else {
            LOGGER.error(format("A PackagingUnit [%s] was deleted but no information about the last Location or the LoadUnit was provided",
                    pu.getPersistentKey()));
        }
    }

    private TransactionBuilder createDefaultBuilder() {
        return TransactionBuilder.aTransactionVO()
                .withCreatedByUser(IdentityContextHolder.getCurrentIdentity())
                .withCategory(CallContextHolder.getOptionalCallContext().map(CallContext::getCaller).orElse(""))
                .withSender(applicationName);
    }
}