/**
* Copyright © 2002 Instituto Superior Técnico
*
* This file is part of FenixEdu Academic.
*
* FenixEdu Academic is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* FenixEdu Academic 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with FenixEdu Academic. If not, see <http://www.gnu.org/licenses/>.
*/
package org.fenixedu.academic.domain.accounting;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.collections.CollectionUtils;
import org.fenixedu.academic.domain.ExecutionYear;
import org.fenixedu.academic.domain.StudentCurricularPlan;
import org.fenixedu.academic.domain.accounting.paymentPlanRules.PaymentPlanRule;
import org.fenixedu.academic.domain.accounting.paymentPlanRules.PaymentPlanRuleManager;
import org.fenixedu.academic.domain.exceptions.DomainException;
import org.fenixedu.academic.util.Bundle;
import org.fenixedu.academic.util.Money;
import org.fenixedu.bennu.core.domain.Bennu;
import org.fenixedu.bennu.core.i18n.BundleUtil;
import org.joda.time.DateTime;
abstract public class PaymentPlan extends PaymentPlan_Base {
protected PaymentPlan() {
super();
super.setRootDomainObject(Bennu.getInstance());
super.setWhenCreated(new DateTime());
}
protected void init(final ExecutionYear executionYear, final Boolean defaultPlan) {
checkParameters(executionYear, defaultPlan);
super.setDefaultPlan(defaultPlan);
super.setExecutionYear(executionYear);
}
private void checkParameters(final ExecutionYear executionYear, final Boolean defaultPlan) {
if (executionYear == null) {
throw new DomainException("error.accounting.PaymentPlan.executionYear.cannot.be.null");
}
if (defaultPlan == null) {
throw new DomainException("error.accounting.PaymentPlan.defaultPlan.cannot.be.null");
}
}
@Override
public void setExecutionYear(ExecutionYear executionYear) {
throw new DomainException("error.accounting.PaymentCondition.cannot.modify.executionYear");
}
@Override
public void setDefaultPlan(Boolean defaultPlan) {
throw new DomainException("error.domain.accounting.PaymentPlan.cannot.modify.defaultPlan");
}
public List<Installment> getInstallmentsSortedByEndDate() {
final List<Installment> result = new ArrayList<Installment>(getInstallmentsSet());
Collections.sort(result, Installment.COMPARATOR_BY_END_DATE);
return result;
}
public Installment getLastInstallment() {
return (getInstallmentsSet().size() == 0) ? null : Collections.max(getInstallmentsSet(), Installment.COMPARATOR_BY_ORDER);
}
public Installment getFirstInstallment() {
return (getInstallmentsSet().size() == 0) ? null : Collections.min(getInstallmentsSet(), Installment.COMPARATOR_BY_ORDER);
}
public int getLastInstallmentOrder() {
final Installment installment = getLastInstallment();
return installment == null ? 0 : installment.getOrder();
}
@Override
public void addInstallments(Installment installment) {
throw new DomainException("error.accounting.PaymentPlan.cannot.add.installment");
}
@Override
public Set<Installment> getInstallmentsSet() {
return Collections.unmodifiableSet(super.getInstallmentsSet());
}
@Override
public void removeInstallments(Installment installment) {
throw new DomainException("error.accounting.PaymentPlan.cannot.remove.installment");
}
public boolean isDefault() {
return getDefaultPlan().booleanValue();
}
public Money calculateOriginalTotalAmount() {
Money result = Money.ZERO;
for (final Installment installment : getInstallmentsSet()) {
result = result.add(installment.getAmount());
}
return result;
}
public Money calculateBaseAmount(final Event event) {
Money result = Money.ZERO;
for (final Installment installment : getInstallmentsSet()) {
result = result.add(installment.calculateBaseAmount(event));
}
return result;
}
public Money calculateTotalAmount(final Event event, final DateTime when, final BigDecimal discountPercentage) {
Money result = Money.ZERO;
for (final Money amount : calculateInstallmentTotalAmounts(event, when, discountPercentage).values()) {
result = result.add(amount);
}
return result;
}
private Map<Installment, Money> calculateInstallmentTotalAmounts(final Event event, final DateTime when,
final BigDecimal discountPercentage) {
final Map<Installment, Money> result = new HashMap<Installment, Money>();
final CashFlowBox cashFlowBox = new CashFlowBox(event, when, discountPercentage);
for (final Installment installment : getInstallmentsSortedByEndDate()) {
result.put(installment, cashFlowBox.calculateTotalAmountFor(installment));
}
return result;
}
private class CashFlowBox {
public DateTime when;
public Money amount;
public DateTime currentTransactionDate;
public List<AccountingTransaction> transactions;
public BigDecimal discountPercentage;
public Event event;
public Money discountValue;
public boolean usedDiscountValue;
private Money discountedValue;
public CashFlowBox(final Event event, final DateTime when, final BigDecimal discountPercentage) {
this.event = event;
this.transactions = new ArrayList<AccountingTransaction>(event.getSortedNonAdjustingTransactions());
this.when = when;
this.discountPercentage = discountPercentage;
this.discountValue = event.getTotalDiscount();
this.discountedValue = Money.ZERO;
this.usedDiscountValue = false;
if (transactions.isEmpty()) {
this.amount = Money.ZERO;
this.currentTransactionDate = when;
} else {
final AccountingTransaction transaction = transactions.remove(0);
this.amount = transaction.getAmountWithAdjustment();
this.currentTransactionDate = transaction.getWhenRegistered();
}
}
private boolean hasMoneyFor(final Money amount) {
return this.amount.greaterOrEqualThan(amount);
}
private boolean hasDiscountValue() {
return this.discountValue.isPositive();
}
public boolean subtractMoneyFor(final Installment installment) {
if (hasDiscountValue() && this.discountValue.greaterOrEqualThan(installment.getAmount())) {
usedDiscountValue = true;
this.discountValue = this.discountValue.subtract(installment.getAmount());
return true;
}
Money installmentAmount =
installment.calculateAmount(this.event, this.currentTransactionDate, this.discountPercentage,
isToApplyPenalty(this.event, installment));
if (hasDiscountValue()) {
installmentAmount = installmentAmount.subtract(this.discountValue);
this.discountedValue = this.discountValue;
}
if (hasMoneyFor(installmentAmount)) {
this.amount = this.amount.subtract(installmentAmount);
this.discountValue = Money.ZERO;
return true;
}
if (this.transactions.isEmpty()) {
return false;
}
final AccountingTransaction transaction = this.transactions.remove(0);
this.amount = this.amount.add(transaction.getAmountWithAdjustment());
this.currentTransactionDate = transaction.getWhenRegistered();
return subtractMoneyFor(installment);
}
public Money subtractRemainingFor(final Installment installment) {
final Money result =
installment
.calculateAmount(this.event, this.when, this.discountPercentage,
isToApplyPenalty(this.event, installment)).subtract(this.discountValue).subtract(this.amount);
this.amount = this.discountValue = Money.ZERO;
return result;
}
public Money calculateTotalAmountFor(final Installment installment) {
final Money result;
if (subtractMoneyFor(installment)) {
if (usedDiscountValue) {
result = Money.ZERO;
} else {
result =
installment.calculateAmount(this.event, this.currentTransactionDate, this.discountPercentage,
isToApplyPenalty(this.event, installment)).subtract(this.discountedValue);
this.discountedValue = Money.ZERO;
}
} else {
result =
installment.calculateAmount(this.event, this.when, this.discountPercentage,
isToApplyPenalty(this.event, installment)).subtract(this.discountedValue);
this.discountedValue = Money.ZERO;
}
usedDiscountValue = false;
return result;
}
}
public Map<Installment, Money> calculateInstallmentRemainingAmounts(final Event event, final DateTime when,
final BigDecimal discountPercentage) {
final Map<Installment, Money> result = new HashMap<Installment, Money>();
final CashFlowBox cashFlowBox = new CashFlowBox(event, when, discountPercentage);
for (final Installment installment : getInstallmentsSortedByEndDate()) {
if (!cashFlowBox.subtractMoneyFor(installment)) {
result.put(installment, cashFlowBox.subtractRemainingFor(installment));
}
}
return result;
}
public Money calculateRemainingAmountFor(final Installment installment, final Event event, final DateTime when,
final BigDecimal discountPercentage) {
final Map<Installment, Money> amountsByInstallment =
calculateInstallmentRemainingAmounts(event, when, discountPercentage);
final Money installmentAmount = amountsByInstallment.get(installment);
return (installmentAmount != null) ? installmentAmount : Money.ZERO;
}
public boolean isInstallmentInDebt(final Installment installment, final Event event, final DateTime when,
final BigDecimal discountPercentage) {
return calculateRemainingAmountFor(installment, event, when, discountPercentage).isPositive();
}
public Installment getInstallmentByOrder(int order) {
for (final Installment installment : getInstallmentsSet()) {
if (installment.getInstallmentOrder() == order) {
return installment;
}
}
return null;
}
public boolean isToApplyPenalty(final Event event, final Installment installment) {
return true;
}
protected void removeParameters() {
super.setExecutionYear(null);
}
public boolean isGratuityPaymentPlan() {
return false;
}
public boolean isCustomGratuityPaymentPlan() {
return false;
}
private boolean hasExecutionYear(final ExecutionYear executionYear) {
return getExecutionYear() != null && getExecutionYear().equals(executionYear);
}
final public boolean isAppliableFor(final StudentCurricularPlan studentCurricularPlan, final ExecutionYear executionYear) {
if (!hasExecutionYear(executionYear)) {
return false;
}
final Collection<PaymentPlanRule> specificRules = getSpecificPaymentPlanRules();
if (specificRules.isEmpty()) {
return false;
}
for (final PaymentPlanRule rule : specificRules) {
if (!rule.isAppliableFor(studentCurricularPlan, executionYear)) {
return false;
}
}
for (final PaymentPlanRule rule : getNotSpecificPaymentRules()) {
if (rule.isEvaluatedInNotSpecificPaymentRules() && rule.isAppliableFor(studentCurricularPlan, executionYear)) {
return false;
}
}
return true;
}
protected Collection<PaymentPlanRule> getNotSpecificPaymentRules() {
/*
* All payment rules could be connected do
* DegreeCurricularPlanServiceAgreementTemplate, but for now are just
* value types
*/
return CollectionUtils.subtract(PaymentPlanRuleManager.getAllPaymentPlanRules(), getSpecificPaymentPlanRules());
}
abstract protected Collection<PaymentPlanRule> getSpecificPaymentPlanRules();
public String getDescription() {
return BundleUtil.getString(Bundle.APPLICATION, this.getClass().getSimpleName() + ".description");
}
public boolean isFor(final ExecutionYear executionYear) {
return getExecutionYear() != null && getExecutionYear().equals(executionYear);
}
abstract public ServiceAgreementTemplate getServiceAgreementTemplate();
public void delete() {
if (!getGratuityEventsWithPaymentPlanSet().isEmpty()) {
throw new DomainException("error.accounting.PaymentPlan.cannot.delete.with.already.associated.gratuity.events");
}
while (!getInstallmentsSet().isEmpty()) {
getInstallmentsSet().iterator().next().delete();
}
removeParameters();
setRootDomainObject(null);
super.deleteDomainObject();
}
public boolean isForPartialRegime() {
return false;
}
public boolean isForFirstTimeInstitutionStudents() {
return false;
}
public boolean hasSingleInstallment() {
return getInstallmentsSet().size() == 1;
}
}