/* * 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.entries.MovementClassType; import br.com.webbudget.domain.model.entity.converter.JPALocalDateConverter; import br.com.webbudget.domain.model.entity.PersistentEntity; 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.OneToMany; 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; /** * A entidade que representa nosso movimento fixo * * @author Arthur Gregorio * * @version 1.0.0 * @since 2.1.0, 18/09/2015 */ @Entity @Table(name = "fixed_movements") @ToString(callSuper = true, of = "code") @EqualsAndHashCode(callSuper = true, of = "code") public class FixedMovement extends PersistentEntity { @Getter @Column(name = "code", nullable = false, length = 8, unique = true) private String code; @Getter @Setter @NotEmpty(message = "{fixed-movement.identification}") @Column(name = "identification", nullable = false, length = 45) private String identification; @Getter @Setter @NotEmpty(message = "{fixed-movement.description}") @Column(name = "description", nullable = false, length = 255) private String description; @Getter @Setter @NotNull(message = "{fixed-movement.value}") @Column(name = "value", nullable = false) private BigDecimal value; @Getter @Setter @Column(name = "quotes") private Integer quotes; @Getter @Setter @Column(name = "auto_launch", nullable = false) private boolean autoLaunch; @Getter @Setter @Column(name = "undetermined", nullable = false) private boolean undetermined; @Getter @Setter @Column(name = "start_date") @Convert(converter = JPALocalDateConverter.class) private LocalDate startDate; @Getter @Setter @Enumerated @Column(name = "fixed_movement_status_type", nullable = false) private FixedMovementStatusType fixedMovementStatusType; /** * 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 = "fixedMovement", fetch = EAGER, cascade = REMOVE) private List<Apportionment> apportionments; @Getter @Setter @Transient private boolean alreadyLaunched; @Getter @Setter @Transient private List<Launch> launches; @Getter @Transient private final List<Apportionment> deletedApportionments; /** * Inicializamos o que for necessario */ public FixedMovement() { this.code = ApplicationUtils.createRamdomCode(5, false); this.autoLaunch = false; this.fixedMovementStatusType = FixedMovementStatusType.ACTIVE; this.apportionments = new ArrayList<>(); this.deletedApportionments = new ArrayList<>(); } /** * * @param apportionment */ 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 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; } /** * 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); } } /** * 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; } /** * @return se este movimento fixo ja finalizou ou nao */ boolean isFinalized() { return this.fixedMovementStatusType == FixedMovementStatusType.FINALIZED; } }