/* * 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.service; import br.com.webbudget.domain.model.entity.entries.CardInvoice; import br.com.webbudget.domain.model.entity.financial.Apportionment; import br.com.webbudget.domain.model.entity.entries.CostCenter; import br.com.webbudget.domain.model.entity.miscellany.FinancialPeriod; import br.com.webbudget.domain.model.entity.financial.FixedMovement; import br.com.webbudget.domain.model.entity.financial.FixedMovementStatusType; import br.com.webbudget.domain.model.entity.financial.Launch; import br.com.webbudget.domain.model.entity.financial.Movement; import br.com.webbudget.domain.model.entity.entries.MovementClass; import br.com.webbudget.domain.model.entity.entries.MovementClassType; import br.com.webbudget.domain.model.entity.financial.MovementStateType; import br.com.webbudget.domain.model.entity.financial.MovementType; import br.com.webbudget.domain.model.entity.financial.Payment; import br.com.webbudget.domain.model.entity.financial.PaymentMethodType; import br.com.webbudget.domain.model.entity.entries.Wallet; import br.com.webbudget.domain.model.entity.entries.WalletBalanceType; import br.com.webbudget.domain.misc.BalanceBuilder; import br.com.webbudget.domain.misc.MovementBuilder; import br.com.webbudget.domain.misc.filter.MovementFilter; import br.com.webbudget.domain.misc.events.UpdateBalance; import br.com.webbudget.domain.misc.ex.InternalServiceError; import br.com.webbudget.application.component.table.Page; import br.com.webbudget.application.component.table.PageRequest; import br.com.webbudget.domain.misc.events.CreateMovement; import br.com.webbudget.domain.misc.events.DeleteMovement; import br.com.webbudget.domain.misc.events.MovementDeleted; import br.com.webbudget.domain.misc.events.MovementPaid; import br.com.webbudget.domain.misc.events.MovementSaved; import br.com.webbudget.domain.misc.events.MovementUpdated; import br.com.webbudget.domain.model.repository.entries.ICardInvoiceRepository; import br.com.webbudget.domain.model.repository.financial.IApportionmentRepository; import br.com.webbudget.domain.model.repository.entries.ICostCenterRepository; import br.com.webbudget.domain.model.repository.financial.IFixedMovementRepository; import br.com.webbudget.domain.model.repository.financial.ILaunchRepository; import br.com.webbudget.domain.model.repository.financial.IMovementRepository; import br.com.webbudget.domain.model.repository.entries.IMovementClassRepository; import br.com.webbudget.domain.model.repository.financial.IPaymentRepository; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.event.Event; import javax.enterprise.event.Observes; import javax.inject.Inject; import javax.transaction.Transactional; import br.com.webbudget.domain.misc.events.PeriodOpened; /** * * @author Arthur Gregorio * * @version 1.2.0 * @since 1.0.0, 04/03/2014 */ @ApplicationScoped public class MovementService { @Inject private ILaunchRepository launchRepository; @Inject private IPaymentRepository paymentRepository; @Inject private IMovementRepository movementRepository; @Inject private ICostCenterRepository costCenterRepository; @Inject private ICardInvoiceRepository cardInvoiceRepository; @Inject private IFixedMovementRepository fixedMovementRepository; @Inject private IApportionmentRepository apportionmentRepository; @Inject private IMovementClassRepository movementClassRepository; @Inject private FinancialPeriodService financialPeriodService; @Inject @MovementPaid private Event<String> movementPaidEvent; @Inject @MovementSaved private Event<String> movementSavedEvent; @Inject @MovementUpdated private Event<String> movementUpdatedEvent; @Inject @MovementDeleted private Event<String> movementDeletedEvent; @Inject @UpdateBalance private Event<BalanceBuilder> updateBalanceEvent; /** * * @param clazz */ @Transactional public void saveMovementClass(MovementClass clazz) { final MovementClass found = this.findMovementClassByNameAndTypeAndCostCenter( clazz.getName(), clazz.getMovementClassType(), clazz.getCostCenter()); if (found != null) { throw new InternalServiceError("error.movement-class.duplicated"); } // valida o orcamento, se estiver ok, salva! this.hasValidBudget(clazz); this.movementClassRepository.save(clazz); } /** * * @param movementClass * @return */ @Transactional public MovementClass updateMovementClass(MovementClass movementClass) { final MovementClass found = this.findMovementClassByNameAndTypeAndCostCenter(movementClass.getName(), movementClass.getMovementClassType(), movementClass.getCostCenter()); if (found != null && !found.equals(movementClass)) { throw new InternalServiceError("error.movement-class.duplicated"); } // valida o orcamento, se estiver ok, salva! this.hasValidBudget(movementClass); return this.movementClassRepository.save(movementClass); } /** * Aqui realizamos a regra de validacao do orcamento pelo centro de custo * * @param movementClass a classe a ser validada */ private boolean hasValidBudget(MovementClass movementClass) { final CostCenter costCenter = movementClass.getCostCenter(); final MovementClassType classType = movementClass.getMovementClassType(); if (costCenter.controlBudget(classType)) { final List<MovementClass> classes = this.listMovementClassesByCostCenterAndType(costCenter, classType); final BigDecimal consumed = classes.stream() .filter(mc -> !mc.equals(movementClass)) .map(MovementClass::getBudget) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal available; if (classType == MovementClassType.IN) { available = costCenter.getRevenuesBudget().subtract(consumed); } else { available = costCenter.getExpensesBudget().subtract(consumed); } // caso o valor disponivel seja menor que o desejado, exception! if (available.compareTo(movementClass.getBudget()) < 0) { final String value = "R$ " + String.format("%10.2f", available); throw new InternalServiceError("error.movement-class.no-budget", value); } } return true; } /** * * @param movementClass */ @Transactional public void deleteMovementClass(MovementClass movementClass) { this.movementClassRepository.delete(movementClass); } /** * Salva o movimento * * @param movement o movimento a ser salvo */ @Transactional public void saveMovement(Movement movement) { // limpa o pagamento se por algum acaso ele vier preenchido movement.setPayment(null); // validamos se os rateios estao corretos movement.validateApportionments(); // se nao foi informada a data de vencimento, seta a data atual if (!movement.hasDueDate()) { movement.setDueDate(LocalDate.now()); } // pega a lista de rateios final List<Apportionment> apportionments = movement.getApportionments(); // salva o movimento movement = this.movementRepository.save(movement); // salva os rateios for (Apportionment apportionment : apportionments) { apportionment.setMovement(movement); this.apportionmentRepository.save(apportionment); } // dispara um evento informando que um movimento foi salvo this.movementSavedEvent.fire(movement.getCode()); } /** * Metodo que observa pedidos de inclusao de movimentos * * @param builder o builder dos movimentos */ @Transactional public void saveMovement(@Observes @CreateMovement MovementBuilder builder) { this.saveMovement(builder.build()); } /** * Atualiza o movimento * * @param movement o movimento a ser atualizado * @return o movimento atualizado */ @Transactional public Movement updateMovement(Movement movement) { // validamos se os rateios estao corretos movement.validateApportionments(); // deleta os rateios ja deletados na view final List<Apportionment> deleteds = movement.getDeletedApportionments(); deleteds.stream().forEach(apportionment -> { this.apportionmentRepository.delete(apportionment); }); // pega a lista de rateios final List<Apportionment> apportionments = movement.getApportionments(); // salva o movimento movement = this.movementRepository.save(movement); // salva os rateios for (Apportionment apportionment : apportionments) { apportionment.setMovement(movement); this.apportionmentRepository.save(apportionment); } movement.getApportionments().clear(); movement.setApportionments(new ArrayList<>( this.apportionmentRepository.listByMovement(movement))); // dispara um evento informando que o movimento foi atualizado this.movementUpdatedEvent.fire(movement.getCode()); return movement; } /** * * @param movement */ @Transactional public void payMovement(Movement movement) { // pega o pagamento Payment payment = movement.getPayment(); // valida o pagamento payment.validatePaymentMethod(); // validamos se os rateios estao corretos movement.validateApportionments(); // se nao foi informada a data de vencimento, seta a data atual if (!movement.hasDueDate()) { movement.setDueDate(LocalDate.now()); } // se pagamos no cartao de credito, coloca o vencimento do movimento // para a data de vencimento da fatura do cartao if (payment.getPaymentMethodType() == PaymentMethodType.CREDIT_CARD) { movement.setDueDate(payment.getCreditCardInvoiceDueDate( movement.getFinancialPeriod())); } // deleta os rateios ja deletados na view final List<Apportionment> deleteds = movement.getDeletedApportionments(); deleteds.stream().forEach(apportionment -> { this.apportionmentRepository.delete(apportionment); }); // pega a lista de rateios final List<Apportionment> apportionments = movement.getApportionments(); // salva o movimento movement = this.movementRepository.save(movement); // salva os rateios for (Apportionment apportionment : apportionments) { apportionment.setMovement(movement); this.apportionmentRepository.save(apportionment); } // salva o pagamento payment = this.paymentRepository.save(payment); // seta no pagamento no movimento e atualiza movement.setPayment(payment); movement.setMovementStateType(MovementStateType.PAID); this.movementRepository.save(movement); movement.getApportionments().clear(); movement.setApportionments(new ArrayList<>( this.apportionmentRepository.listByMovement(movement))); // atualizamos os saldos das carteiras quando pagamento em dinheiro if (payment.getPaymentMethodType() == PaymentMethodType.IN_CASH || payment.getPaymentMethodType() == PaymentMethodType.DEBIT_CARD) { Wallet wallet; if (payment.getPaymentMethodType() == PaymentMethodType.DEBIT_CARD) { wallet = payment.getCard().getWallet(); } else { wallet = payment.getWallet(); } // atualizamos o novo saldo final BalanceBuilder builder = new BalanceBuilder(); final BigDecimal oldBalance = wallet.getBalance(); builder.forWallet(wallet) .withOldBalance(oldBalance) .withMovementedValue(movement.getValue()) .referencingMovement(movement.getCode()); if (movement.isExpense()) { builder.withActualBalance(oldBalance.subtract(movement.getValue())) .andType(WalletBalanceType.PAYMENT); } else { builder.withActualBalance(oldBalance.add(movement.getValue())) .andType(WalletBalanceType.REVENUE); } this.updateBalanceEvent.fire(builder); } // dispara um evento informando que o movimento foi pago this.movementPaidEvent.fire(movement.getCode()); } /** * * @param movement */ @Transactional public void deleteMovement(Movement movement) { // se tem vinculo com fatura, nao pode ser excluido if (movement.isCardInvoicePaid()) { throw new InternalServiceError("error.movement.paid-invoice"); } // se for movimento fixo e for a ultima parcela reabre o movimento fixo if (movement.isLastLaunch()) { final FixedMovement fixedMovement = movement.getLaunch().getFixedMovement(); fixedMovement.setFixedMovementStatusType( FixedMovementStatusType.ACTIVE); this.fixedMovementRepository.save(fixedMovement); } // devolve o saldo na carteira se for o caso if (movement.getMovementStateType() == MovementStateType.PAID && movement.getPayment().getPaymentMethodType() == PaymentMethodType.IN_CASH) { Wallet paymentWallet; final Payment payment = movement.getPayment(); if (payment.getPaymentMethodType() == PaymentMethodType.DEBIT_CARD) { paymentWallet = payment.getCard().getWallet(); } else { paymentWallet = payment.getWallet(); } final BigDecimal movimentedValue; // se entrada, valor negativo, se saida valor positivo if (movement.isExpense()) { movimentedValue = movement.getValue(); } else { movimentedValue = movement.getValue().negate(); } // tratamos a devoluacao do saldo final BalanceBuilder builder = new BalanceBuilder(); builder.forWallet(paymentWallet) .withOldBalance(paymentWallet.getBalance()) .withActualBalance(paymentWallet.getBalance().add(movimentedValue)) .withMovementedValue(movimentedValue) .andType(WalletBalanceType.BALANCE_RETURN); this.updateBalanceEvent.fire(builder); } // deleta o movimento this.movementRepository.delete(movement); // dispara o evento indicando que o movimento foi deletado this.movementDeletedEvent.fire(movement.getCode()); } /** * Escuta por possiveis pedidos de exclusao de movimentos via evento * * @param code o codigo do movimento a ser deletado */ @Transactional public void deleteMovement(@Observes @DeleteMovement String code) { // busca o movimento final Movement movement = this.movementRepository.findByCode(code); // checa e se achar, envia para exclusao if (movement == null) { throw new InternalServiceError("error.movement.not-found", code); } // checa se o periodo permite deletar o movimento if (movement.getFinancialPeriod().isClosed()) { throw new InternalServiceError("error.movement.closed-period", code); } this.deleteMovement(movement); } /** * Metodo que realiza o processo de deletar um movimento vinculado a uma * fatura de cartao, deletando a fatura e limpando as flags dos movimentos * vinculados a ela * * @param movement o movimento referenciando a invoice */ @Transactional public void deleteCardInvoiceMovement(Movement movement) { final CardInvoice cardInvoice = this.cardInvoiceRepository.findByMovement(movement); // listamos os movimentos da invoice final List<Movement> invoiceMovements = this.movementRepository.listByCardInvoice(cardInvoice); // limpamos as flags para cada moveimento invoiceMovements.stream().map((invoiceMovement) -> { invoiceMovement.setCardInvoice(null); return invoiceMovement; }).map((invoiceMovement) -> { invoiceMovement.setCardInvoicePaid(false); return invoiceMovement; }).forEach((invoiceMovement) -> { this.movementRepository.save(invoiceMovement); }); // se houve pagamento, devolve o valor para a origem if (movement.getMovementStateType() == MovementStateType.PAID) { final Wallet paymentWallet = movement.getPayment().getWallet(); final BigDecimal oldBalance = paymentWallet.getBalance(); final BigDecimal newBalance = oldBalance.add(movement.getValue()); final BalanceBuilder builder = new BalanceBuilder(); builder.forWallet(paymentWallet) .withOldBalance(oldBalance) .withActualBalance(newBalance) .withMovementedValue(movement.getValue()) .andType(WalletBalanceType.BALANCE_RETURN); this.updateBalanceEvent.fire(builder); } // deletamos a movimentacao da invoice this.movementRepository.delete(movement); // deletamos a invoice this.cardInvoiceRepository.delete(cardInvoice); } /** * * @param costCenter */ @Transactional public void saveCostCenter(CostCenter costCenter) { final CostCenter found = this.findCostCenterByNameAndParent(costCenter.getName(), costCenter.getParentCostCenter()); if (found != null) { throw new InternalServiceError("error.cost-center.duplicated"); } this.costCenterRepository.save(costCenter); } /** * * @param costCenter * @return */ @Transactional public CostCenter updateCostCenter(CostCenter costCenter) { final CostCenter found = this.findCostCenterByNameAndParent(costCenter.getName(), costCenter.getParentCostCenter()); if (found != null && !found.equals(costCenter)) { throw new InternalServiceError("error.cost-center.duplicated"); } return this.costCenterRepository.save(costCenter); } /** * * @param costCenter */ @Transactional public void deleteCostCenter(CostCenter costCenter) { this.costCenterRepository.delete(costCenter); } /** * * @param fixedMovement * @return */ @Transactional public FixedMovement saveFixedMovement(FixedMovement fixedMovement) { // valida os rateios fixedMovement.validateApportionments(); if (!fixedMovement.isUndetermined() && (fixedMovement.getQuotes() == null || fixedMovement.getQuotes() == 0)) { throw new InternalServiceError("error.fixed-movement.no-quotes"); } // pega os rateios antes de salvar o movimento para nao perder a lista final List<Apportionment> apportionments = fixedMovement.getApportionments(); // salva o movimento fixedMovement = this.fixedMovementRepository.save(fixedMovement); // salva os rateios for (Apportionment apportionment : apportionments) { apportionment.setFixedMovement(fixedMovement); this.apportionmentRepository.save(apportionment); } // busca novamente os movimentos fixos para virem com todos os campos fixedMovement.getApportionments().clear(); fixedMovement.setApportionments(new ArrayList<>( this.apportionmentRepository.listByFixedMovement(fixedMovement))); return fixedMovement; } /** * * @param fixedMovement */ @Transactional public void deleteFixedMovement(FixedMovement fixedMovement) { final List<Launch> launches = this.launchRepository .listByFixedMovement(fixedMovement); if (launches != null && !launches.isEmpty()) { throw new InternalServiceError("error.fixed-movement.has-launches", fixedMovement.getIdentification()); } this.fixedMovementRepository.delete(fixedMovement); } /** * * @param fixedMovements * @param period */ @Transactional public void launchFixedMovements(List<FixedMovement> fixedMovements, FinancialPeriod period) { fixedMovements .stream() .forEach(fixedMovement -> { // constroi o movimento final MovementBuilder movementBuilder = new MovementBuilder(); movementBuilder .withValue(fixedMovement.getValue()) .onDueDate(period.getEnd()) .describedBy(fixedMovement.getDescription()) .inThePeriodOf(period) .dividedAmong(fixedMovement.getApportionments()); final Movement movement = this.updateMovement(movementBuilder.build()); // criamos o lancamento final Launch launch = new Launch(); // setamos em que parcela estamos se for um parcelamento if (!fixedMovement.isUndetermined()) { final Integer totalQuotes = this.launchRepository .countByFixedMovement(fixedMovement).intValue(); launch.setQuote(totalQuotes + 1); // se chegamos na ultima parcela, encerramos if (launch.getQuote().equals(fixedMovement.getQuotes())) { fixedMovement.setFixedMovementStatusType( FixedMovementStatusType.FINALIZED); this.fixedMovementRepository.save(fixedMovement); } // atualizamos a descricao do movimento final StringBuilder stringBuilder = new StringBuilder(); stringBuilder .append(fixedMovement.getIdentification()) .append(" ") .append(launch.getQuote()) .append("/") .append(fixedMovement.getQuotes()); movement.setDescription(stringBuilder.toString()); launch.setMovement( this.movementRepository.save(movement)); } else { launch.setMovement(movement); } launch.setFinancialPeriod(period); launch.setFixedMovement(fixedMovement); this.launchRepository.save(launch); }); } /** * * @param period */ public void autoLaunchFixedMovements(@Observes @PeriodOpened FinancialPeriod period) { final List<FixedMovement> fixedMovements = this.fixedMovementRepository.listAutoLaunch(); this.launchFixedMovements(fixedMovements, period); } /** * * @param movementClassId * @return */ public MovementClass findMovementClassById(long movementClassId) { return this.movementClassRepository.findById(movementClassId, false); } /** * * @param costCenterId * @return */ public CostCenter findCostCenterById(long costCenterId) { return this.costCenterRepository.findById(costCenterId, false); } /** * * @param movementId * @return */ public Movement findMovementById(long movementId) { return this.movementRepository.findById(movementId, false); } /** * * @param fixedMovementId * @return */ public FixedMovement findFixedMovementById(long fixedMovementId) { return this.fixedMovementRepository.findById(fixedMovementId, false); } /** * * @param movement * @return */ public LocalDate findStartDateByMovement(Movement movement) { final Launch launch = this.launchRepository.findByMovement(movement); if (launch != null) { return launch.getStartDate(); } else { return null; } } /** * * @param isBlocked * @return */ public List<MovementClass> listMovementClasses(Boolean isBlocked) { return this.movementClassRepository.listByStatus(isBlocked); } /** * * @param isBlocked * @param pageRequest * @return */ public Page<MovementClass> listMovementClassesLazily(Boolean isBlocked, PageRequest pageRequest) { return this.movementClassRepository.listLazilyByStatus(isBlocked, pageRequest); } /** * * @param costCenter * @param type * @return */ public List<MovementClass> listMovementClassesByCostCenterAndType(CostCenter costCenter, MovementClassType type) { return this.movementClassRepository.listByCostCenterAndType(costCenter, type); } /** * * @param isBlocked * @return */ public List<CostCenter> listCostCenters(Boolean isBlocked) { return this.costCenterRepository.listByStatus(isBlocked); } /** * * @param isBlocked * @param pageRequest * @return */ public Page<CostCenter> listCostCentersLazily(Boolean isBlocked, PageRequest pageRequest) { return this.costCenterRepository.listLazilyByStatus(isBlocked, pageRequest); } /** * * @param financialPeriod * @return */ public List<Movement> listMovementsByPeriod(FinancialPeriod financialPeriod) { return this.movementRepository.listByPeriodAndStateAndType( financialPeriod, null, null); } /** * Diferente do metodo que busca todas os movimentos por periodo, este busca * apenas os movimentos do tipo movimento, desconsiderando todos que sao do * tipo card invoice * * @param period o periodo * @return a lista de movimentos */ public List<Movement> listOnlyMovementsByPeriod(FinancialPeriod period) { MovementStateType state = MovementStateType.PAID; if (period.isClosed()) { state = MovementStateType.CALCULATED; } return this.movementRepository.listByPeriodAndStateAndType( period, state, MovementType.MOVEMENT); } /** * * @return */ public List<Movement> listMovementsByOpenFinancialPeriod() { return this.movementRepository.listByActiveFinancialPeriod(); } /** * * @param dueDate * @param showOverdue * @return */ public List<Movement> listMovementsByDueDate(LocalDate dueDate, boolean showOverdue) { return this.movementRepository.listByDueDate(dueDate, showOverdue); } /** * * @param name * @param type * @param costCenter * @return */ public MovementClass findMovementClassByNameAndTypeAndCostCenter(String name, MovementClassType type, CostCenter costCenter) { return this.movementClassRepository.findByNameAndTypeAndCostCenter(name, type, costCenter); } /** * * @param name * @param parent * @return */ public CostCenter findCostCenterByNameAndParent(String name, CostCenter parent) { return this.costCenterRepository.findByNameAndParent(name, parent); } /** * * @param cardInvoice * @return */ public List<Movement> listMovementsByCardInvoice(CardInvoice cardInvoice) { return this.movementRepository.listByCardInvoice(cardInvoice); } /** * * @param filter * @param pageRequest * @return */ public Page<Movement> listMovementsByFilter(MovementFilter filter, PageRequest pageRequest) { return this.movementRepository.listByFilter(filter, pageRequest); } /** * * @param filter * @param pageRequest * @return */ public Page<FixedMovement> listFixedMovementsByFilter(String filter, PageRequest pageRequest) { final Page<FixedMovement> page = this.fixedMovementRepository.listByFilter(filter, pageRequest); final FinancialPeriod period = this.financialPeriodService.findActiveFinancialPeriod(); page.getContent() .forEach(fixedMovement -> { final List<Launch> launches = this.launchRepository .listByFixedMovement(fixedMovement); for (Launch launch : launches) { if (launch.belongsToPeriod(period)) { fixedMovement.setAlreadyLaunched(true); break; } } }); return page; } /** * * @param movementCode * @return */ public Movement findMovementByCode(String movementCode) { return this.movementRepository.findByCode(movementCode); } /** * * @param fixedMovement * @return */ public FixedMovement fetchLaunchesForFixedMovement(FixedMovement fixedMovement) { final List<Launch> launches = this.launchRepository.listByFixedMovement(fixedMovement); fixedMovement.setLaunches(launches); return fixedMovement; } /** * * @param fixedMovement * @param pageRequest * @return */ public Page<Launch> listLaunchesByFixedMovement(FixedMovement fixedMovement, PageRequest pageRequest) { return this.launchRepository.listByFixedMovement(fixedMovement, pageRequest); } }