TransportUnit.java

/*
 * Copyright 2005-2024 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.common.transport;

import jakarta.persistence.AttributeOverride;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.validation.constraints.NotNull;
import org.ameba.integration.jpa.ApplicationEntity;
import org.ameba.integration.jpa.BaseEntity;
import org.hibernate.envers.AuditOverride;
import org.hibernate.envers.Audited;
import org.hibernate.envers.NotAudited;
import org.openwms.common.app.Default;
import org.openwms.common.location.Location;
import org.openwms.common.transport.barcode.Barcode;
import org.openwms.common.transport.reservation.TransportUnitReservation;
import org.openwms.core.units.api.Weight;
import org.openwms.core.values.CoreTypeDefinitions;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.util.Assert;

import java.beans.ConstructorProperties;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

import static org.hibernate.envers.RelationTargetAuditMode.NOT_AUDITED;

/**
 * A TransportUnit is a physical item like a box, a toad, a bin, a pallet etc., used as a container that is moved between warehouse
 * {@code Location}s and might carry goods or other items like {@code LoadUnit}s on top. A TransportUnit must have some kind of identifier,
 * like a physical Barcode, an RFID tag or others. There might be projects where TransportUnits are solely identified by virtual identifiers
 * and don't have any physical identifiers. A TransportUnit may even carry other TransportUnits.
 *
 * @author Heiko Scherrer
 * @GlossaryTerm
 */
@Configurable
@Audited(targetAuditMode = NOT_AUDITED)
@AuditOverride(forClass = ApplicationEntity.class)
@AuditOverride(forClass = BaseEntity.class)
@Entity
@Table(name = "COM_TRANSPORT_UNIT", uniqueConstraints = @UniqueConstraint(name = "COM_TRANSPORT_UNIT_BARCODE", columnNames = {"C_BARCODE"}))
public class TransportUnit extends ApplicationEntity implements Serializable {

    /** Unique natural key. */
    @Embedded
    @AttributeOverride(name = "value", column = @Column(name = "C_BARCODE", length = Barcode.BARCODE_LENGTH, nullable = false))
    @OrderBy
    private Barcode barcode;

    /** Indicates whether the {@code TransportUnit} is empty or not (nullable). */
    @Column(name = "C_EMPTY")
    private Boolean empty;

    /** A {@code TransportUnit} may belong to a group of {@code TransportUnits}. */
    @Column(name = "C_GROUP_NAME")
    private String groupName;

    /** Date when the {@code TransportUnit} has been moved to the current {@link Location}. */
    @Column(name = "C_ACTUAL_LOCATION_DATE")
    private LocalDateTime actualLocationDate;

    /** Weight of the {@code TransportUnit}. */
    @Embedded
    @AttributeOverride(name = "unitType", column = @Column(name = "C_WEIGHT_UOM", length = CoreTypeDefinitions.QUANTITY_LENGTH))
    @AttributeOverride(name = "magnitude", column = @Column(name = "C_WEIGHT"))
    private Weight weight = Weight.ZERO;

    /** State of the {@code TransportUnit}. */
    @Column(name = "C_STATE")
    private String state = TransportUnitState.AVAILABLE.name();

    /** The current {@link Location} of the {@code TransportUnit}. */
    @ManyToOne
    @JoinColumn(name = "C_ACTUAL_LOCATION", nullable = false, foreignKey = @ForeignKey(name = "COM_TU_FK_LOC_ACTUAL"))
    private Location actualLocation;

    /** The target {@link Location} of the {@code TransportUnit}. This property is set when a {@code TransportOrder} is started. */
    @ManyToOne
    @JoinColumn(name = "C_TARGET_LOCATION", foreignKey = @ForeignKey(name = "COM_TU_FK_LOC_TARGET"))
    private Location targetLocation;

    /** The {@link TransportUnitType} of the {@code TransportUnit}. */
    @ManyToOne
    @JoinColumn(name = "C_TRANSPORT_UNIT_TYPE", nullable = false, foreignKey = @ForeignKey(name = "COM_TU_FK_TUT"))
    private TransportUnitType transportUnitType;

    /** Owning {@code TransportUnit}. */
    @ManyToOne
    @JoinColumn(name = "C_PARENT", foreignKey = @ForeignKey(name = "COM_TU_FK_TU_PARENT"))
    private TransportUnit parent;

    /** The {@code User} who performed the last inventory action on the {@code TransportUnit}. */
    @Column(name = "C_INVENTORY_USER")
    private String inventoryUser;

    /** Date of last inventory check. */
    @Column(name = "C_INVENTORY_DATE")
    private LocalDateTime inventoryDate;

    /** A set of all child {@code TransportUnit}s, ordered by id. */
    @OneToMany(mappedBy = "parent", cascade = {CascadeType.MERGE, CascadeType.PERSIST})
    @OrderBy("actualLocationDate DESC")
    private Set<TransportUnit> children = new HashSet<>();

    /** A List of errors occurred on the {@code TransportUnit}. */
    @OneToMany(mappedBy = "transportUnit", cascade = {CascadeType.ALL})
    @NotAudited
    private List<UnitError> errors = new ArrayList<>(0);

    /** Tracks all active {@link TransportUnitReservation}s on this {@link TransportUnit}. */
    @OneToMany(mappedBy = "transportUnit", cascade = {CascadeType.ALL})
    private List<TransportUnitReservation> reservations;

    
    /*~ ----------------------------- constructors ------------------- */
    /** Dear JPA... */
    protected TransportUnit() { }

    @Default
    @ConstructorProperties({"barcode"})
    public TransportUnit(@NotNull Barcode barcode) {
        Assert.notNull(barcode, "Barcode must not be null");
        this.barcode = barcode;
        initInventory();
    }

    /**
     * Create a new {@code TransportUnit} with an unique {@link Barcode}.
     *
     * @param barcode The unique identifier of this {@code TransportUnit} is the {@link Barcode} - must not be {@literal null}
     * @param transportUnitType The {@code TransportUnitType} of this {@code TransportUnit} - must not be {@literal null}
     * @param actualLocation The current {@code Location} of this {@code TransportUnit} - must not be {@literal null}
     * @throws IllegalArgumentException when one of the params is {@literal null}
     */
    @ConstructorProperties({"barcode", "transportUnitType", "actualLocation"})
    public TransportUnit(@NotNull Barcode barcode, @NotNull TransportUnitType transportUnitType, @NotNull Location actualLocation) {
        Assert.notNull(barcode, "Barcode must not be null");
        Assert.notNull(transportUnitType, "TransportUnitType must not be null");
        Assert.notNull(actualLocation, "ActualLocation must not be null");
        this.barcode = barcode;
        setTransportUnitType(transportUnitType);
        setActualLocation(actualLocation);
        initInventory();
    }

    /*~ ----------------------------- methods ------------------- */
    /** Required for the Mapper. */
    @Override
    public void setPersistentKey(String pKey) {
        super.setPersistentKey(pKey);
    }

    /**
     * Get the actual {@link Location} of the {@code TransportUnit}.
     *
     * @return The {@link Location} where the {@code TransportUnit} is placed on
     */
    public Location getActualLocation() {
        return actualLocation;
    }

    /**
     * Place the {@code TransportUnit} and all its children to a {@link Location}.
     *
     * @param actualLocation The new {@link Location} of the {@code TransportUnit} and all its children
     * @throws IllegalArgumentException when {@code actualLocation} is {@literal null}
     */
    public void setActualLocation(Location actualLocation) {
        Assert.notNull(actualLocation, "ActualLocation must not be null, this: " + this);
        this.actualLocation = actualLocation;
        this.actualLocationDate = LocalDateTime.now();
        this.actualLocation.setLastMovement(this.actualLocationDate);
        if (this.getChildren() != null) {
            this.getChildren().forEach(child -> child.setActualLocation(actualLocation));
        }
    }

    /**
     * Initialize inventory info of the {@code TransportUnit}.
     */
    public void initInventory() {
        setInventoryUser("init");
        setInventoryDate(LocalDateTime.now());
    }

    /**
     * Get the target {@link Location} of the {@code TransportUnit}. This property can not be {@literal null} when an active {@code
     * TransportOrder} exists.
     *
     * @return The target location
     */
    public Location getTargetLocation() {
        return this.targetLocation;
    }

    /**
     * Set the target {@link Location} of the {@code TransportUnit}. Shall only be set in combination with an active {@code
     * TransportOrder}.
     *
     * @param targetLocation The target {@link Location} where this {@code TransportUnit} shall be transported to
     */
    public void setTargetLocation(Location targetLocation) {
        this.targetLocation = targetLocation;
    }

    /**
     * Indicates whether the {@code TransportUnit} is empty or not.
     *
     * @return {@literal true} if empty, {@literal false} if not empty, {@literal null} when not defined
     */
    public Boolean getEmpty() {
        return this.empty;
    }

    /**
     * Marks the {@code TransportUnit} to be empty.
     *
     * @param empty {@literal true} to mark the {@code TransportUnit} as empty, {@literal false} to mark it as not empty and {@literal null}
     * for no definition
     */
    public void setEmpty(Boolean empty) {
        this.empty = empty;
    }

    /**
     * Get the groupId.
     *
     * @return The groupId
     */
    public String getGroupName() {
        return groupName;
    }

    /**
     * Set the groupId.
     *
     * @param groupId The groupId
     */
    public void setGroupName(String groupId) {
        this.groupName = groupId;
    }

    /**
     * Returns the username of the User who performed the last inventory action on the {@code TransportUnit}.
     *
     * @return The username who did the last inventory check
     */
    public String getInventoryUser() {
        return this.inventoryUser;
    }

    /**
     * Set the username who performed the last inventory action on the {@code TransportUnit}.
     *
     * @param inventoryUser The username who did the last inventory check
     */
    public void setInventoryUser(String inventoryUser) {
        this.inventoryUser = inventoryUser;
    }

    /**
     * Number of {@code TransportUnit}s belonging to the {@code TransportUnit}.
     *
     * @return The number of all {@code TransportUnit}s belonging to this one
     */
    public int getNoTransportUnits() {
        return this.children.size();
    }

    /**
     * Returns the date when the {@code TransportUnit} moved to the actualLocation.
     *
     * @return The timestamp when the {@code TransportUnit} moved the last time
     */
    public LocalDateTime getActualLocationDate() {
        return actualLocationDate;
    }

    /**
     * Returns the timestamp of the last inventory check of the {@code TransportUnit}.
     *
     * @return The timestamp of the last inventory check of the {@code TransportUnit}.
     */
    public LocalDateTime getInventoryDate() {
        return inventoryDate;
    }

    /**
     * Set the timestamp of the last inventory action of the {@code TransportUnit}.
     *
     * @param inventoryDate The timestamp of the last inventory check
     * @throws IllegalArgumentException when {@code inventoryDate} is {@literal null}
     */
    public void setInventoryDate(LocalDateTime inventoryDate) {
        Assert.notNull(inventoryDate, () -> "InventoryDate must not be null, this: " + this);
        this.inventoryDate = inventoryDate;
    }

    /**
     * Returns the current weight of the {@code TransportUnit}.
     *
     * @return The current weight of the {@code TransportUnit}
     */
    public Weight getWeight() {
        return weight;
    }

    /**
     * Sets the current weight of the {@code TransportUnit}.
     *
     * @param weight The current weight of the {@code TransportUnit}
     */
    public void setWeight(Weight weight) {
        this.weight = weight;
    }

    public List<UnitError> getErrors() {
        return this.errors;
    }

    /**
     * Add an error to the {@code TransportUnit}.
     *
     * @param error An {@link UnitError} to be added
     * @return The key.
     * @throws IllegalArgumentException when {@code error} is {@literal null}
     */
    public UnitError addError(UnitError error) {
        Assert.notNull(error, () -> "Error must not be null, this: " + this);
        error.setTransportUnit(this);
        this.errors.add(error);
        return error;
    }

    /**
     * Checks whether this {@code TransportUnit} has one or more reservations.
     *
     * @return {@literal true} if so
     */
    public boolean hasReservations() {
        return this.reservations != null && !this.reservations.isEmpty();
    }

    /**
     * Return the state of the {@code TransportUnit}.
     *
     * @return The current state of the {@code TransportUnit}
     */
    public String getState() {
        return this.state;
    }

    /**
     * Set the state of the {@code TransportUnit}.
     *
     * @param state The state to set on the {@code TransportUnit}
     */
    public void setState(String state) {
        this.state = state;
    }

    /**
     * Return the {@link TransportUnitType} of the {@code TransportUnit}.
     *
     * @return The {@link TransportUnitType} the {@code TransportUnit} belongs to
     */
    public TransportUnitType getTransportUnitType() {
        return this.transportUnitType;
    }

    /**
     * Set the {@link TransportUnitType} of the {@code TransportUnit}.
     *
     * @param transportUnitType The type of the {@code TransportUnit}
     */
    public void setTransportUnitType(TransportUnitType transportUnitType) {
        Assert.notNull(transportUnitType, () -> "TransportUnitType must not be null, this: " + this);
        this.transportUnitType = transportUnitType;
    }

    /**
     * Return the {@link Barcode} of the {@code TransportUnit}.
     *
     * @return The current {@link Barcode}
     */
    public Barcode getBarcode() {
        return barcode;
    }

    /**
     * Returns the parent {@code TransportUnit}.
     *
     * @return the parent.
     */
    public TransportUnit getParent() {
        return parent;
    }

    /**
     * Set a parent {@code TransportUnit}.
     *
     * @param parent The parent to set.
     */
    public void setParent(TransportUnit parent) {
        this.parent = parent;
    }

    /**
     * Get all child {@code TransportUnit}s.
     *
     * @return the transportUnits.
     */
    public Set<TransportUnit> getChildren() {
        return this.children;
    }

    /**
     * Add a {@code TransportUnit} to the children.
     *
     * @param transportUnit The {@code TransportUnit} to be added to the list of children
     * @throws IllegalArgumentException when transportUnit is {@literal null}
     */
    public void addChild(TransportUnit transportUnit) {
        Assert.notNull(transportUnit, () -> "TransportUnitType must not be null, this: " + this);
        if (transportUnit.hasParent()) {
            if (transportUnit.getParent().equals(this)) {

                // if this instance is already the parent, we just return
                return;
            }

            // disconnect post from it's current relationship
            transportUnit.getParent().removeChild(transportUnit);
        }

        // make this instance the new parent
        transportUnit.setParent(this);
        this.children.add(transportUnit);
    }

    /**
     * Checks whether this {@code TransportUnit} has a parent {@code TransportUnit} or not.
     *
     * @return {@code true} it has a parent, otherwise {@code false}
     */
    public boolean hasParent() {
        return parent != null;
    }

    /**
     * Checks whether this {@code TransportUnit} has child {@code TransportUnit}s or not.
     *
     * @return {@code true} it has children, otherwise {@code false}
     */
    public boolean hasChildren() {
        return this.children != null && !this.children.isEmpty();
    }

    /**
     * Remove a {@code TransportUnit} from the list of children.
     *
     * @param transportUnit The {@code TransportUnit} to be removed from the list of children
     * @throws IllegalArgumentException when {@code transportUnit} is {@literal null} or not a child of this instance
     */
    public void removeChild(TransportUnit transportUnit) {
        Assert.notNull(transportUnit, () -> "TransportUnit must not be null, this: " + this);
        // make sure this is the parent before we break the relationship
        if (transportUnit.parent == null || !transportUnit.parent.equals(this)) {
            throw new IllegalArgumentException("Child TransportUnit not associated with this instance, this: " + this);
        }
        transportUnit.setParent(null);
        this.children.remove(transportUnit);
    }

    /**
     * {@inheritDoc}
     *
     * Return the {@link Barcode} as String.
     */
    @Override
    public String toString() {
        return barcode.toString();
    }

    /**
     * {@inheritDoc}
     *
     * Uses barcode for comparison.
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        TransportUnit that = (TransportUnit) o;
        return barcode.equals(that.barcode);
    }

    /**
     * {@inheritDoc}
     *
     * Uses barcode for calculation.
     */
    @Override
    public int hashCode() {
        return Objects.hash(barcode);
    }
}