/*
* Copyright (C) 2015 Arthur Gregorio, AG.Software
*
* 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 br.com.webbudget.domain.model.entity.financial;
import br.com.webbudget.domain.model.entity.miscellany.FinancialPeriod;
import br.com.webbudget.domain.model.entity.entries.CostCenter;
import br.com.webbudget.domain.model.entity.entries.MovementClassType;
import br.com.webbudget.domain.model.entity.converter.JPALocalDateConverter;
import br.com.webbudget.domain.model.entity.entries.Contact;
import br.com.webbudget.domain.model.entity.PersistentEntity;
import br.com.webbudget.domain.model.entity.entries.CardInvoice;
import br.com.webbudget.domain.misc.ex.InternalServiceError;
import br.com.webbudget.infraestructure.configuration.ApplicationUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import static javax.persistence.CascadeType.REMOVE;
import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.Enumerated;
import static javax.persistence.FetchType.EAGER;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.validation.constraints.NotNull;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import org.hibernate.validator.constraints.NotEmpty;
/**
*
* @author Arthur Gregorio
*
* @version 1.0.0
* @since 1.0.0, 04/03/2014
*/
@Entity
@Table(name = "movements")
@ToString(callSuper = true, of = "code")
@EqualsAndHashCode(callSuper = true, of = "code")
public class Movement extends PersistentEntity {
@Getter
@Column(name = "code", nullable = false, length = 8, unique = true)
private String code;
@Getter
@Setter
@NotNull(message = "{movement.value}")
@Column(name = "value", nullable = false, length = 8)
private BigDecimal value;
@Getter
@Setter
@NotEmpty(message = "{movement.description}")
@Column(name = "description", nullable = false, length = 255)
private String description;
@Getter
@Setter
@Convert(converter = JPALocalDateConverter.class)
@Column(name = "due_date", nullable = false)
private LocalDate dueDate;
@Getter
@Setter
@Enumerated
@Column(name = "movement_state_type", nullable = false)
private MovementStateType movementStateType;
@Getter
@Setter
@Enumerated
@Column(name = "movement_type", nullable = false)
private MovementType movementType;
@Getter
@Setter
@Column(name = "card_invoice_paid")
private boolean cardInvoicePaid;
@Getter
@Setter
@OneToOne(mappedBy = "movement", cascade = REMOVE)
private Launch launch;
@Getter
@Setter
@OneToOne(cascade = REMOVE)
@JoinColumn(name = "id_payment")
private Payment payment;
@Getter
@Setter
@ManyToOne
@JoinColumn(name = "id_contact")
private Contact contact;
@Getter
@Setter
@ManyToOne
@JoinColumn(name = "id_card_invoice")
private CardInvoice cardInvoice;
@Getter
@Setter
@ManyToOne
@NotNull(message = "{movement.financial-period}")
@JoinColumn(name = "id_financial_period", nullable = false)
private FinancialPeriod financialPeriod;
/**
* Fetch eager pois sempre que precisarmos pesquisar um movimento, vamos
* precisar saber como ele foi distribuido, ou seja, precisaremos do rateio
*/
@Getter
@Setter
@Fetch(FetchMode.SUBSELECT)
@OneToMany(mappedBy = "movement", fetch = EAGER, cascade = REMOVE)
private List<Apportionment> apportionments;
/**
* Atributo usado para o controle da view no momento de checar se um deter-
* minado movimento foi ou nao conferido na fatura do cartao
*/
@Getter
@Setter
@Transient
private boolean checked;
@Getter
@Transient
private final List<Apportionment> deletedApportionments;
/**
*
*/
public Movement() {
this.code = ApplicationUtils.createRamdomCode(5, false);
this.apportionments = new ArrayList<>();
this.deletedApportionments = new ArrayList<>();
this.cardInvoicePaid = false;
this.movementType = MovementType.MOVEMENT;
this.movementStateType = MovementStateType.OPEN;
}
/**
* Metodo para adicao de rateios ao movimento
*
* @param apportionment o rateio a ser adiocionado
*/
public void addApportionment(Apportionment apportionment) {
// checa se nao esta sendo inserido outro exatamente igual
if (this.apportionments.contains(apportionment)) {
throw new InternalServiceError("error.apportionment.duplicated");
}
// checa se nao esta inserindo outro para o mesmo CC e MC
for (Apportionment a : this.apportionments) {
if (a.getCostCenter().equals(apportionment.getCostCenter())
&& a.getMovementClass().equals(apportionment.getMovementClass())) {
throw new InternalServiceError("error.apportionment.duplicated");
}
}
// verificamos se os movimentos partem na mesma direcao, para que nao
// haja rateios com debitos e creditos juntos
if (!this.apportionments.isEmpty()) {
final MovementClassType direction = this.getDirection();
final MovementClassType apportionmentDirection
= apportionment.getMovementClass().getMovementClassType();
if ((direction == MovementClassType.IN && apportionmentDirection == MovementClassType.OUT)
|| (direction == MovementClassType.OUT && apportionmentDirection == MovementClassType.IN)) {
throw new InternalServiceError("error.apportionment.mix-of-classes");
}
}
// impossivel ter um rateio com valor igual a zero
if (apportionment.getValue().compareTo(BigDecimal.ZERO) == 0
|| apportionment.getValue().compareTo(this.value) > 0) {
throw new InternalServiceError("error.apportionment.invalid-value");
}
this.apportionments.add(apportionment);
}
/**
* Remove um rateio pelo seu codigo, caso nao localize o mesmo dispara uma
* exception para informor ao usuario que nao podera fazer nada pois sera um
* problema do sistema...
*
* LOL WHAT!?
*
* @param code o codigo
*/
public void removeApportionment(String code) {
final Apportionment toRemove = this.apportionments.stream()
.filter(apportionment -> apportionment.getCode().equals(code))
.findFirst()
.orElseThrow(() -> new InternalServiceError(
"error.apportionment.not-found", code));
// se o rateio ja foi salvo, adicionamos ele em outra lista
// para que quando salvar o movimento ele seja deletado
if (toRemove.isSaved()) {
this.deletedApportionments.add(toRemove);
}
// remove da lista principal
this.apportionments.remove(toRemove);
}
/**
* @return o nome do contato vinculado ao movimento
*/
public String getContactName() {
return this.contact != null ? this.contact.getName() : "";
}
/**
* @return o valor da somatoria dos rateios
*/
public BigDecimal getApportionmentsTotal() {
return this.apportionments
.stream()
.map(Apportionment::getValue)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
* @return o valor a ser rateado descontando o valor ja rateado
*/
public BigDecimal getValueToDivide() {
return this.value.subtract(this.getApportionmentsTotal());
}
/**
* @return se existe ou nao valores para serem rateados
*/
public boolean hasValueToDivide() {
return this.getApportionmentsTotal().compareTo(this.value) == 0;
}
/**
* @return se este movimento foi pago com cartao de credito
*/
public boolean isPaidOnCreditCard() {
return this.isExpense() && this.payment.getPaymentMethodType()
== PaymentMethodType.CREDIT_CARD;
}
/**
* @return se este movimento foi pago em cartao de debito
*/
public boolean isPaidOnDebitCard() {
return this.isExpense() && this.payment.getPaymentMethodType()
== PaymentMethodType.DEBIT_CARD;
}
/**
* @return se este movimento eh uma fatura de cartao ou nao
*/
public boolean isCardInvoice() {
return this.movementType == MovementType.CARD_INVOICE;
}
/**
* @return se este movimento nao eh uma fatura de cartao
*/
public boolean isNotCardInvoice() {
return this.movementType != MovementType.CARD_INVOICE;
}
/**
* @return se temos um movimento editavel
*/
public boolean isEditable() {
return (this.movementStateType == MovementStateType.OPEN
&& !this.financialPeriod.isClosed());
}
/**
* @return se o movimento esta pago ou nao
*/
public boolean isPaid() {
return this.movementStateType == MovementStateType.PAID
|| this.movementStateType == MovementStateType.CALCULATED;
}
/**
* @return se temos um movimento pagavel
*/
public boolean isPayable() {
return this.movementStateType == MovementStateType.OPEN
&& !this.financialPeriod.isClosed();
}
/**
* @return se temos um movimento deletavel
*/
public boolean isDeletable() {
return (this.movementStateType == MovementStateType.OPEN
|| this.movementStateType == MovementStateType.PAID)
&& !this.financialPeriod.isClosed();
}
/**
* @return se o movimento esta vencido ou nao
*/
public boolean isOverdue() {
return this.dueDate.isBefore(LocalDate.now());
}
/**
* @return se temos ou nao nao movimento a data de pagamento setada
*/
public boolean hasDueDate() {
return this.dueDate != null;
}
/**
* @return se este movimento e uma despesa
*/
public boolean isExpense() {
return this.apportionments
.stream()
.findFirst()
.map(apportionment -> apportionment.isForExpenses())
.orElse(false);
}
/**
* @return se este movimento e uma receita
*/
public boolean isRevenue() {
return this.apportionments
.stream()
.findFirst()
.map(apportionment -> apportionment.isForRevenues())
.orElse(false);
}
/**
* @return a data de pagamento deste movimento
*/
public LocalDate getPaymentDate() {
return this.payment.getPaymentDate();
}
/**
* @return todos os centros de custo que este movimento faz parte
*/
public List<CostCenter> getCostCenters() {
final List<CostCenter> costCenters = new ArrayList<>();
this.apportionments.stream().forEach((apportionment) -> {
costCenters.add(apportionment.getMovementClass().getCostCenter());
});
return costCenters;
}
/**
* De acordo com a primeira classe do rateio, diz se o movimento e de
* entrada ou saida
*
* @return a direcao do movimento de acordo com as classes usadas
*/
public MovementClassType getDirection() {
for (Apportionment apportionment : this.apportionments) {
return apportionment.getMovementClass().getMovementClassType();
}
return null;
}
/**
* Realiza a validacao dos rateios do movimento fixo
*/
public void validateApportionments() {
if (this.getApportionments().isEmpty()) {
throw new InternalServiceError(
"error.apportionment.empty-apportionment");
} else if (this.getApportionmentsTotal().compareTo(this.value) > 0) {
final String difference = String.format("%10.2f",
this.getApportionmentsTotal().subtract(this.value));
throw new InternalServiceError(
"error.apportionment.gt-value", difference);
} else if (this.getApportionmentsTotal().compareTo(this.value) < 0) {
final String difference = String.format("%10.2f",
this.value.subtract(this.getApportionmentsTotal()));
throw new InternalServiceError(
"error.apportionment.lt-value", difference);
}
}
/**
* @return se este movimento e o ultimo lancamento de uma serie de
* lancamentos de movimentos fixos
*/
public boolean isLastLaunch() {
return this.launch != null && this.launch.getFixedMovement().isFinalized();
}
}