TransportUnit.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;

import jakarta.persistence.AttributeOverride;
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.Table;
import jakarta.persistence.Transient;
import jakarta.persistence.UniqueConstraint;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import org.ameba.i18n.Translator;
import org.ameba.integration.jpa.ApplicationEntity;
import org.openwms.values.Synchronizable;
import org.openwms.wms.api.TimeProvider;
import org.openwms.wms.location.Location;
import org.openwms.wms.transport.barcode.Barcode;
import org.openwms.wms.transport.commands.TransportUnitCommand;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.format.annotation.DateTimeFormat;

import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.ServiceLoader;

import static org.openwms.wms.api.TimeProvider.DATE_TIME_WITH_TIMEZONE;

/**
 * A TransportUnit.
 *
 * @author Heiko Scherrer
 */
@Entity
@Table(name = "WMS_INV_TRANSPORT_UNIT", uniqueConstraints = {
        @UniqueConstraint(name = "UC_INV_TU_BARCODE", columnNames = {"C_TRANSPORT_UNIT_BK"}),
        @UniqueConstraint(name = "UC_INV_TU_FOREIGN_PID", columnNames = {"C_FOREIGN_PID"})
})
public class TransportUnit extends ApplicationEntity implements Serializable, Synchronizable {

    @Transient
    private final transient TimeProvider timeProvider = ServiceLoader.load(TimeProvider.class).iterator().next();

    /** The foreign persistent key of the {@code Location}. */
    @Column(name = "C_FOREIGN_PID")
    private String foreignPKey;

    /** The business key of the {@code TransportUnit}. */
    @Embedded
    @AttributeOverride(name = "value", column = @Column(name = "C_TRANSPORT_UNIT_BK", nullable = false))
    @NotNull
    private Barcode transportUnitBK;

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

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

    /** The current target {@code Location} coordinate or {@code LocationGroup} name of the {@code TransportUnit}. */
    @Column(name = "C_TARGET")
    private String target;

    /** State of the {@code TransportUnit}. */
    @Column(name = "C_STATE")
    private String state = DEFAULT_STATE;
    /** The default for the TU state. */
    public static final String DEFAULT_STATE = "AVAILABLE";

    /** The {@code TransportUnitType} of the {@code TransportUnit}. */
    @Column(name = "C_TRANSPORT_UNIT_TYPE", nullable = false)
    @NotEmpty
    private String transportUnitType;

    /** The current length of the {@code TransportUnit}. */
    @Column(name = "C_LENGTH")
    private Integer length;

    /** The current width of the {@code TransportUnit}. */
    @Column(name = "C_WIDTH")
    private Integer width;

    /** The current height of the {@code TransportUnit}. */
    @Column(name = "C_HEIGHT")
    private Integer height;

    /** Whether this instance has been synchronized with the master data source correctly. */
    @Column(name = "C_ACKNOWLEDGED")
    private boolean acknowledged;

    /** An optional assignment to a customer order. */
    @Column(name = "C_CUSTOMER_ORDER_ID")
    private String customerOrderId;

    /** The {@code User} who performed the last reconciliation on the {@code TransportUnit}. */
    @Column(name = "C_RECONCILED_BY")
    private String reconciledBy;

    /** Date of last reconciliation. */
    @Column(name = "C_RECONCILED_AT", columnDefinition = "timestamp(0)")
    @DateTimeFormat(pattern = DATE_TIME_WITH_TIMEZONE)
    private ZonedDateTime reconciledAt;

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

    private TransportUnit(Builder builder) {
        super.setPersistentKey(builder.pKey);
        foreignPKey = builder.foreignPKey;
        if (builder.transportUnitBK != null) {
            setTransportUnitBK(builder.transportUnitBK.getValue());
        }
        actualLocation = builder.actualLocation;
        actualLocationDate = builder.actualLocationDate;
        target = builder.target;
        setState(builder.state);
        transportUnitType = builder.transportUnitType;
        length = builder.length;
        width = builder.width;
        height = builder.height;
        acknowledged = builder.acknowledged;
        customerOrderId = builder.customerOrderId;
        reconciledBy = builder.reconciledBy;
        reconciledAt = builder.reconciledAt;
    }

    public static Builder newBuilder() {
        return new Builder();
    }

    /*~ -------------------------- methods -------------------------- */

    /**
     * Block the TransportUnit from being used.
     *
     * @param eventPublisher An instance of the EventPublisher in order to send proper events if this happens
     * @deprecated Unused
     */
    @Deprecated(forRemoval = true)
    public void block(final ApplicationEventPublisher eventPublisher) {
        this.setState("BLOCKED");
        var tuCommand = new TransportUnitCommand(TransportUnitCommand.Type.BLOCK, this);
        eventPublisher.publishEvent(tuCommand);
    }

    /**
     * @see Location#verifyFreeSpaceAvailable(Translator, int)
     */
    public void validateAndSetActualLocation(final Translator translator, final Location actualLocation, final int numberOfTransportUnits) {
        actualLocation.verifyFreeSpaceAvailable(translator, numberOfTransportUnits);
        this.actualLocation = actualLocation;
        this.actualLocationDate = timeProvider.nowAsZonedDateTime().toLocalDateTime();
    }

    public void synchronizeActualLocationChange(Location actualLocation, LocalDateTime actualLocationDate) {
        setActualLocation(actualLocation);
        setActualLocationDate(actualLocationDate);
    }

    /*~ -------------------------- accessors -------------------------- */
    public String getForeignPKey() {
        return foreignPKey;
    }

    public void setForeignPKey(String foreignPKey) {
        this.foreignPKey = foreignPKey;
    }

    public Barcode getTransportUnitBK() {
        return transportUnitBK;
    }

    public void setTransportUnitBK(String transportUnitBK) {
        this.transportUnitBK = Barcode.of(transportUnitBK);
    }

    public Location getActualLocation() {
        return actualLocation;
    }

    /**
     * Set the actualLocation.
     *
     * @param actualLocation The actualLocation to set
     * @deprecated Used for the Mapper only, don't call from application code.
     */
    @Deprecated
    public void setActualLocation(Location actualLocation) {
        this.actualLocation = actualLocation;
    }

    public LocalDateTime getActualLocationDate() {
        return actualLocationDate;
    }

    /**
     * Set the actualLocationDate.
     *
     * @param actualLocationDate The actualLocationDate to set
     * @deprecated Used for the Mapper only, don't call from application code.
     */
    @Deprecated
    private void setActualLocationDate(LocalDateTime actualLocationDate) {
        this.actualLocationDate = actualLocationDate;
    }

    public String getTarget() {
        return target;
    }

    public void setTarget(String target) {
        this.target = target;
    }

    public String getState() {
        return state;
    }

    public void setState(String state) {
        if (state == null) {
            state = DEFAULT_STATE;
        }
        this.state = state;
    }

    public String getTransportUnitType() {
        return transportUnitType;
    }

    public void setTransportUnitType(String transportUnitType) {
        this.transportUnitType = transportUnitType;
    }

    public Integer getLength() {
        return length;
    }

    public void setLength(Integer length) {
        this.length = length;
    }

    public Integer getWidth() {
        return width;
    }

    public void setWidth(Integer width) {
        this.width = width;
    }

    public Integer getHeight() {
        return height;
    }

    public void setHeight(Integer height) {
        this.height = height;
    }

    public void acknowledge() {
        this.acknowledged = true;
    }

    public boolean isAcknowledged() {
        return acknowledged;
    }

    public String getCustomerOrderId() {
        return customerOrderId;
    }

    public void setCustomerOrderId(String customerOrderId) {
        this.customerOrderId = customerOrderId;
    }

    public String getReconciledBy() {
        return reconciledBy;
    }

    public void setReconciledBy(String reconciledBy) {
        this.reconciledBy = reconciledBy;
    }

    public ZonedDateTime getReconciledAt() {
        return reconciledAt;
    }

    public void setReconciledAt(ZonedDateTime reconciledAt) {
        this.reconciledAt = reconciledAt;
    }

    @Override
    public void setOl(long ol) {
        super.setOl(ol);
    }

    public static final class Builder {
        private String pKey;
        private String foreignPKey;
        private @NotEmpty Barcode transportUnitBK;
        private Location actualLocation;
        private LocalDateTime actualLocationDate;
        private String target;
        private String state;
        private String transportUnitType;
        private Integer length;
        private Integer width;
        private Integer height;
        private boolean acknowledged;
        private String customerOrderId;
        private String reconciledBy;
        private ZonedDateTime reconciledAt;

        private Builder() {
        }

        public Builder foreignPKey(@NotEmpty String val) {
            foreignPKey = val;
            return this;
        }

        public Builder pKey(@NotEmpty String val) {
            pKey = val;
            return this;
        }

        public Builder transportUnitBK(@NotEmpty Barcode val) {
            transportUnitBK = val;
            return this;
        }

        public Builder actualLocation(Location val) {
            actualLocation = val;
            return this;
        }

        public Builder actualLocationDate(LocalDateTime val) {
            actualLocationDate = val;
            return this;
        }

        public Builder target(String val) {
            target = val;
            return this;
        }

        public Builder state(String val) {
            if (val == null || val.isEmpty()) {
                state = DEFAULT_STATE;
            }
            state = val;
            return this;
        }

        public Builder transportUnitType(String val) {
            transportUnitType = val;
            return this;
        }

        public Builder length(Integer val) {
            length = val;
            return this;
        }

        public Builder width(Integer val) {
            width = val;
            return this;
        }

        public Builder height(Integer val) {
            height = val;
            return this;
        }

        public Builder acknowledged(boolean val) {
            acknowledged = val;
            return this;
        }

        public Builder customerOrderId(String val) {
            customerOrderId = val;
            return this;
        }

        public Builder reconciledBy(String val) {
            reconciledBy = val;
            return this;
        }

        public Builder reconciledAt(ZonedDateTime val) {
            reconciledAt = val;
            return this;
        }

        public TransportUnit build() {
            return new TransportUnit(this);
        }
    }

    @Override
    public String toString() {
        return transportUnitBK == null ? "" : transportUnitBK.toString();
    }

    /**
     * {@inheritDoc}
     *
     * All fields.
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof TransportUnit)) return false;
        if (!super.equals(o)) return false;
        TransportUnit that = (TransportUnit) o;
        return acknowledged == that.acknowledged && Objects.equals(timeProvider, that.timeProvider) && Objects.equals(foreignPKey, that.foreignPKey) && Objects.equals(transportUnitBK, that.transportUnitBK) && Objects.equals(actualLocation, that.actualLocation) && Objects.equals(actualLocationDate, that.actualLocationDate) && Objects.equals(target, that.target) && Objects.equals(state, that.state) && Objects.equals(transportUnitType, that.transportUnitType) && Objects.equals(length, that.length) && Objects.equals(width, that.width) && Objects.equals(height, that.height) && Objects.equals(customerOrderId, that.customerOrderId) && Objects.equals(reconciledBy, that.reconciledBy) && Objects.equals(reconciledAt, that.reconciledAt);
    }

    /**
     * {@inheritDoc}
     *
     * All fields.
     */
    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), timeProvider, foreignPKey, transportUnitBK, actualLocation, actualLocationDate, target, state, transportUnitType, length, width, height, acknowledged, customerOrderId, reconciledBy, reconciledAt);
    }
}