/*
* Copyright (C) 2014 GG-Net GmbH - Oliver Günther
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package eu.ggnet.dwoss.redtape.entity;
import java.io.Serializable;
import java.util.*;
import javax.persistence.*;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import eu.ggnet.dwoss.redtape.entity.Document.Condition;
import eu.ggnet.dwoss.redtape.entity.Document.Directive;
import eu.ggnet.dwoss.rules.*;
import eu.ggnet.dwoss.util.persistence.EagerAble;
import eu.ggnet.dwoss.util.persistence.entity.IdentifiableEntity;
import static eu.ggnet.dwoss.rules.DocumentType.*;
import lombok.*;
/**
* The Dossier.
* <p>
* Rules for a valid Dossier:
* <ul>
* <li>Must have at least one active {@link Document} of {@link DocumentType#ORDER} or {@link DocumentType#INVOICE}</li>
* <li>May have at most one active {@link Document} of {@link DocumentType#ORDER} and one active {@link Document} of {@link DocumentType#INVOICE}</li>
* <li>May have multiple active {@link Document} of {@link DocumentType#CREDIT_MEMO}</li>
* </ul>
* <p>
* @has 1 - 2 Address
* @has 1 - n Dossier.Directive
* @has m - n Dossier.Condition
* @has 1 - n DirectiveHistory
* @has 1 - n ConditionHistory
* @has 1 - n Document
* @author bastian.venz, oliver.guenther, pascal.perau
*/
@Entity
@NamedQueries({
@NamedQuery(name = "Dossier.byCustomerId", query = "select d from Dossier d where d.customerId = ?1"),
@NamedQuery(name = "Dossier.byCustomerIdAndClosed", query = "select d from Dossier as d where d.customerId = ?1 and d.closed = ?2 ORDER BY d.identifier DESC"),
@NamedQuery(name = "Dossier.byDossierIds", query = "select d from Dossier d where d.id in (?1)"),
@NamedQuery(name = "Dossier.byClosed", query = "select d from Dossier as d where d.closed = ?1"),
@NamedQuery(name = "Dossier.byIdentifier", query = "select d from Dossier d where d.identifier like ?1 ORDER BY d.identifier DESC"),
@NamedQuery(name = "Dossier.allDescending", query = "select d from Dossier d ORDER BY d.id DESC")
})
public class Dossier extends IdentifiableEntity implements Serializable, EagerAble {
/**
* Comperator for an inverse order using the actual date of the first active document.
*/
public static final Comparator<Dossier> ORDER_INVERSE_ACTIVE_ACTUAL = (Dossier o1, Dossier o2) -> {
Document d1 = o1.getActiveDocuments().get(0);
Document d2 = o2.getActiveDocuments().get(0);
if ( d1.equals(d2) ) return 0;
if ( d1.getActual().equals(d2.getActual()) ) {
if ( d1.getConditions().contains(Document.Condition.CANCELED) ) return +1;
return -1;
}
return d2.getActual().compareTo(d1.getActual());
};
public static NavigableSet<Long> toIds(Collection<Dossier> dossiers) {
NavigableSet<Long> result = new TreeSet<>();
for (Dossier dos : dossiers) {
result.add(dos.getId());
}
return result;
}
@Id
@GeneratedValue
private long id;
@Version
private Short optLock = 0;
@Lob
@Column(length = 65536)
private String comment;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "dossier")
@Valid
Set<Document> documents = new HashSet<>();
@Enumerated
private PaymentMethod paymentMethod;
@Min(1)
private long customerId;
private boolean dispatch;
/**
* Indicates, that this Dossier has no Documents in a state, which demand something.
*/
private boolean closed;
/**
* This String identifies the Dossier.
*/
private String identifier;
@Valid
@Embedded
private Reminder reminder;
/**
* A non persisted value to show any external system, that this is not a real but a wrapped legacy instance.
*/
@Transient
@Getter
@Setter
private boolean legacy;
/**
* A non persisted value to set some identifier to be handled by a legacy system.
*/
@Transient
@Getter
@Setter
private String legacyIdentifier;
public Dossier() {
}
public Dossier(PaymentMethod paymentMethod, boolean dispatch, long customerId) {
this.paymentMethod = paymentMethod;
this.customerId = customerId;
this.dispatch = dispatch;
}
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
@Override
public long getId() {
return id;
}
public long getCustomerId() {
return customerId;
}
public void setCustomerId(long customerId) {
this.customerId = customerId;
}
public PaymentMethod getPaymentMethod() {
return paymentMethod;
}
public void setPaymentMethod(PaymentMethod paymentMethod) {
this.paymentMethod = paymentMethod;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
public Reminder getReminder() {
return reminder;
}
public void setReminder(Reminder reminder) {
this.reminder = reminder;
}
/**
* Generates a list of all {@link Position#uniqueUnitId}s of {@link PositionType#UNIT} that are relevant to the recent state of the Dossier. <br />
* If active Documents of {@link DocumentType#BLOCK}, {@link DocumentType#RETURNS} or {@link DocumentType#CAPITAL_ASSET} exist, all uniqueUniteIds are
* taken.<br />
* Any of the found positions used in a successor of {@link DocumentType#INVOICE} is subtracted from the list.<br />
* The following cases will always return an empty list:
* <ul>
* <li>The Dossier is closed</li>
* <li>Only an active {@link Document} of {@link Type#ORDER} exists and is canceled</li>
* </ul>
* <p/>
* @return a list of all {@link Position#uniqueUnitId} of {@link Position.Type#UNIT} that are relevant to the recent state of the Dossier.
*/
public Set<Integer> getRelevantUniqueUnitIds() {
Set<Integer> unitIds = new HashSet<>();
if ( closed ) return unitIds;
if ( getActiveDocuments().size() == 1 && getActiveDocuments().get(0).getConditions().contains(Document.Condition.CANCELED) ) return unitIds;
Document doc = null;
if ( !getActiveDocuments(DocumentType.BLOCK).isEmpty() ) doc = getActiveDocuments(DocumentType.BLOCK).get(0);
else if ( !getActiveDocuments(DocumentType.RETURNS).isEmpty() ) doc = getActiveDocuments(DocumentType.RETURNS).get(0);
else if ( !getActiveDocuments(DocumentType.CAPITAL_ASSET).isEmpty() ) doc = getActiveDocuments(DocumentType.CAPITAL_ASSET).get(0);
else if ( getActiveDocuments(DocumentType.INVOICE).isEmpty() ) doc = getActiveDocuments(DocumentType.ORDER).get(0);
if ( doc != null ) {
for (Position position : doc.getPositions(PositionType.UNIT).values()) {
unitIds.add(position.getUniqueUnitId());
}
return unitIds;
}
// From here on it's clear that a Invoice exists, so only the Repaymend Options must be subtracted.
unitIds.addAll(getActiveDocuments(DocumentType.INVOICE).get(0).getPositionsUniqueUnitIds());
Set<Integer> successors = new HashSet<>();
if ( !getActiveDocuments(DocumentType.CREDIT_MEMO).isEmpty() )
successors.addAll(getActiveDocuments(DocumentType.CREDIT_MEMO).get(0).getPositionsUniqueUnitIds());
if ( !getActiveDocuments(DocumentType.ANNULATION_INVOICE).isEmpty() )
successors.addAll(getActiveDocuments(DocumentType.ANNULATION_INVOICE).get(0).getPositionsUniqueUnitIds());
for (Integer uuId : successors) {
unitIds.remove(uuId);
}
return unitIds;
}
public Dossier add(Document document) {
if ( document == null ) return this;
document.setDossier(this);
return this;
}
public Document remove(Document document) {
if ( document == null ) return null;
document.setDossier(null);
return document;
}
public Set<Document> getDocuments() {
return Collections.unmodifiableSet(documents);
}
/**
* Returns all {@link Document} with {@link Document#active}<code>=true</code> and the supplied {@link DocumentType}.
* <p>
* @param types the type to filter by.
* @return a List of Documents.
*/
public List<Document> getActiveDocuments(DocumentType... types) {
List<Document> result = new ArrayList<>();
for (Document document : documents) {
if ( document.isActive() ) result.add(document);
}
if ( types == null || types.length == 0 ) return result;
List<DocumentType> typesList = Arrays.asList(types);
for (Iterator<Document> it = result.iterator(); it.hasNext();) {
if ( !typesList.contains(it.next().getType()) ) it.remove();
}
return result;
}
public boolean isClosed() {
return closed;
}
public boolean isDispatch() {
return dispatch;
}
public void setDispatch(boolean dispatch) {
this.dispatch = dispatch;
}
/**
* Returns the most important Document.
* <p/>
* @return the most important Document.
*/
public Document getCrucialDocument() {
List<DocumentType> typeOrder = Arrays.asList(ANNULATION_INVOICE, CREDIT_MEMO, COMPLAINT);
for (DocumentType type : typeOrder) {
for (Document document : getActiveDocuments(type)) {
if ( !document.isClosed() ) return document;
}
}
if ( !getActiveDocuments(INVOICE).isEmpty() ) return getActiveDocuments(INVOICE).get(0);
return getActiveDocuments().get(0);
}
/**
* Returns the crucial directive for this dossier.
*
* @return the crucial directive
*/
public Document.Directive getCrucialDirective() {
if ( !this.getActiveDocuments(DocumentType.CREDIT_MEMO).isEmpty() ) {
for (Document document : getActiveDocuments(DocumentType.CREDIT_MEMO)) {
if ( document.getDirective() == Directive.BALANCE_REPAYMENT ) return Directive.BALANCE_REPAYMENT;
}
return Directive.NONE;
}
if ( !this.getActiveDocuments(DocumentType.ANNULATION_INVOICE).isEmpty() ) {
for (Document document : getActiveDocuments(DocumentType.ANNULATION_INVOICE)) {
if ( document.getDirective() == Directive.BALANCE_REPAYMENT ) return Directive.BALANCE_REPAYMENT;
}
return Directive.NONE;
}
if ( !this.getActiveDocuments(DocumentType.COMPLAINT).isEmpty() ) {
for (Document document : getActiveDocuments(DocumentType.COMPLAINT)) {
if ( document.getConditions().contains(Condition.REJECTED) || document.getConditions().contains(Condition.WITHDRAWN) )
return this.getActiveDocuments(DocumentType.INVOICE).get(0).getDirective();
}
return this.getActiveDocuments(DocumentType.COMPLAINT).get(0).getDirective();
}
if ( !this.getActiveDocuments(DocumentType.INVOICE).isEmpty() ) return this.getActiveDocuments(DocumentType.INVOICE).get(0).getDirective();
if ( !this.getActiveDocuments(DocumentType.ORDER).isEmpty() ) return this.getActiveDocuments(DocumentType.ORDER).get(0).getDirective();
if ( !getActiveDocuments().isEmpty() ) return getActiveDocuments().get(0).getDirective();
return Directive.NONE;
}
/**
* Returns true, if two dossiers equal or the changes differ in a way that the states allow them.
* Allowed Chages are:
* <ul>
* <li>If closed only Address and the Reminder</li>
* <li>If not closed, but the active Invoice/Order is closed only Address and the Reminder</li>
* <li>Else everything</li>
* </ul>
*
* @param dos the {@link Dossier}
* @return true, if two dossiers equal or the changes differ in a way that the states allow them.
*/
public boolean changesAllowed(Dossier dos) {
boolean restricted = closed;
if ( !restricted ) {
if ( !getActiveDocuments(DocumentType.INVOICE).isEmpty() && getActiveDocuments(DocumentType.INVOICE).get(0).isClosed() ) {
restricted = true;
}
}
if ( !restricted ) {
if ( !getActiveDocuments(DocumentType.ORDER).isEmpty() && getActiveDocuments(DocumentType.ORDER).get(0).isClosed() ) {
restricted = true;
}
}
if ( !restricted ) return true;
// So we are restricted
if ( this.paymentMethod != dos.paymentMethod ) return false;
if ( this.customerId != dos.customerId ) return false;
if ( this.dispatch != dos.dispatch ) return false;
if ( !Objects.equals(this.identifier, dos.identifier) ) return false;
return true;
}
@Override
public void fetchEager() {
documents.size();
}
/**
* This method sets the closed state of a Dossier.
* <p/>
* @param closed
*/
public void setClosed(boolean closed) {
this.closed = closed;
}
@Override
public String toString() {
List<String> docs = new ArrayList<>();
for (Document d : documents) {
docs.add("Document{" + d.getId() + "," + d.getType() + ", active=" + d.isActive() + "}");
}
return "Dossier{" + "id=" + id + ", comment=" + comment + ", documents=" + docs + ", paymentMethod=" + paymentMethod + ", customerId="
+ customerId + ", dispatch=" + dispatch + ", closed=" + closed + ", identifier=" + identifier + ", reminder=" + reminder + '}';
}
public String toSimpleLine() {
StringBuilder sb = new StringBuilder("Dossier{id=");
sb.append(id).append(",identifier=").append(identifier);
if ( closed ) sb.append(",closed");
sb.append(",paymentMethod=").append(paymentMethod);
sb.append(",directive=").append(getCrucialDirective());
sb.append("}");
return sb.toString();
}
public String toMultiLine() {
return toMultiLine(false, true);
}
/**
* Returns a multi line representation of the Dossier.
* <p>
* @param showActiveOnly show only active documents.
* @param showPositions show also positions.
* @return a multi line representation of the Dossier
*/
public String toMultiLine(boolean showActiveOnly, boolean showPositions) {
StringBuilder sb = new StringBuilder(toSimpleLine());
for (Document doc : (showActiveOnly ? getActiveDocuments() : documents)) {
sb.append("\n - ").append(doc.toSimpleLine());
if ( !showPositions ) continue;
for (Position pos : doc.getPositions().values()) {
sb.append("\n - ").append(pos.toSimpleLine());
}
}
return sb.toString();
}
}