MIFOSX-376: track when an installment is completed in full.

This commit is contained in:
Keith Woodlock
2013-06-05 20:59:10 +01:00
parent efbb1e3b2f
commit 06f2da8443
13 changed files with 549 additions and 485 deletions
@@ -0,0 +1,5 @@
ALTER TABLE `m_loan_repayment_schedule`
ADD COLUMN `obligations_met_on_date` DATE NULL DEFAULT NULL AFTER `completed_derived`;
ALTER TABLE `m_loan_repayment_schedule`
CHANGE COLUMN `interest_waived_derived` `interest_waived_derived` DECIMAL(19,6) NULL DEFAULT NULL AFTER `interest_writtenoff_derived` ;
@@ -43,6 +43,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
for (LoanRepaymentScheduleInstallment currentInstallment : installments) {
currentInstallment.resetDerivedComponents();
currentInstallment.updateDerivedFields(currency, disbursementDate);
}
// re-process loan charges over repayment periods (picking up on waived
@@ -108,7 +109,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
for (LoanRepaymentScheduleInstallment currentInstallment : installments) {
if (currentInstallment.isNotFullyCompleted()) {
if (currentInstallment.isNotFullyPaidOff()) {
// is this transaction early/late/on-time with respect to the
// current installment?
@@ -205,6 +206,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
public void handleWriteOff(final LoanTransaction loanTransaction, final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments) {
LocalDate transactionDate = loanTransaction.getTransactionDate();
Money principalPortion = Money.zero(currency);
Money interestPortion = Money.zero(currency);
Money feeChargesPortion = Money.zero(currency);
@@ -214,11 +216,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
// principal, interest and charges
for (LoanRepaymentScheduleInstallment currentInstallment : installments) {
if (currentInstallment.isNotFullyCompleted()) {
principalPortion = principalPortion.plus(currentInstallment.writeOffOutstandingPrincipal(currency));
interestPortion = interestPortion.plus(currentInstallment.writeOffOutstandingInterest(currency));
feeChargesPortion = feeChargesPortion.plus(currentInstallment.writeOffOutstandingFeeCharges(currency));
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.writeOffOutstandingPenaltyCharges(currency));
if (currentInstallment.isNotFullyPaidOff()) {
principalPortion = principalPortion.plus(currentInstallment.writeOffOutstandingPrincipal(transactionDate, currency));
interestPortion = interestPortion.plus(currentInstallment.writeOffOutstandingInterest(transactionDate, currency));
feeChargesPortion = feeChargesPortion.plus(currentInstallment.writeOffOutstandingFeeCharges(transactionDate, currency));
penaltychargesPortion = penaltychargesPortion.plus(currentInstallment.writeOffOutstandingPenaltyCharges(transactionDate,
currency));
}
}
@@ -24,105 +24,96 @@ import org.mifosplatform.organisation.monetary.domain.Money;
* If the entire principal of an installment is paid in advance then the
* interest component is waived.
*/
public class CreocoreLoanRepaymentScheduleTransactionProcessor extends
AbstractLoanRepaymentScheduleTransactionProcessor {
public class CreocoreLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
/**
* For creocore, early is defined as any date before the installment due
* date
*/
@SuppressWarnings("unused")
@Override
protected boolean isTransactionInAdvanceOfInstallment(
final int currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> installments,
final LocalDate transactionDate, final Money transactionAmount) {
/**
* For creocore, early is defined as any date before the installment due
* date
*/
@SuppressWarnings("unused")
@Override
protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate, final Money transactionAmount) {
LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
return transactionDate.isBefore(currentInstallment.getDueDate());
}
return transactionDate.isBefore(currentInstallment.getDueDate());
}
/**
* For early/'in advance' repayments, pay off in the same way as on-time payments,
* interest first then principal.
*/
@SuppressWarnings("unused")
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance);
}
/**
* For early/'in advance' repayments, pay off in the same way as on-time
* payments, interest first then principal.
*/
@SuppressWarnings("unused")
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance) {
/**
* For late repayments, pay off in the same way as on-time payments,
* interest first then principal.
*/
@SuppressWarnings("unused")
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance);
}
return handleTransactionThatIsOnTimePaymentOfInstallment(
currentInstallment, loanTransaction,
transactionAmountUnprocessed);
}
/**
* For late repayments, pay off in the same way as on-time payments,
* interest first then principal.
*/
@SuppressWarnings("unused")
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
/**
* For normal on-time repayments, pays off interest first, then principal.
*/
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed);
}
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isChargesWaiver()) {
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(loanTransaction.getPenaltyChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(loanTransaction.getFeeChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
} else if (loanTransaction.isInterestWaiver()) {
interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
} else {
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
return transactionAmountRemaining;
}
/**
* For normal on-time repayments, pays off interest first, then principal.
*/
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) {
@SuppressWarnings("unused")
@Override
protected void onLoanOverpayment(final LoanTransaction loanTransaction,
final Money loanOverPaymentAmount) {
// dont do anything for with loan over-payment
}
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isChargesWaiver()) {
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
loanTransaction.getPenaltyChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment
.waiveFeeChargesComponent(transactionDate, loanTransaction.getFeeChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
} else if (loanTransaction.isInterestWaiver()) {
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
} else {
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
return transactionAmountRemaining;
}
@SuppressWarnings("unused")
@Override
protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) {
// dont do anything for with loan over-payment
}
}
@@ -55,7 +55,7 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
lastInstallmentDueDate = previousInstallment.getDueDate();
isInAdvance = !(transactionDate.isAfter(lastInstallmentDueDate) || (transactionDate.isEqual(lastInstallmentDueDate)));
return isInAdvance;
}
@@ -75,16 +75,16 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
Money penaltyChargesPortion = Money.zero(currency);
if (loanTransaction.isInterestWaiver()) {
interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
} else {
if (currentInstallment.isPrincipalNotCompleted(currency)) {
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
if (currentInstallment.isPrincipalCompleted(currency)) {
// FIXME - KW - if auto waiving interest need to create
// another transaction to handle this.
currentInstallment.waiveInterestComponent(currentInstallment.getInterestCharged(currency));
currentInstallment.waiveInterestComponent(transactionDate, currentInstallment.getInterestCharged(currency));
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
@@ -93,16 +93,16 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
}
// 1. pay of principal with over payment.
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionAmountRemaining);
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionAmountRemaining);
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
}
@@ -117,6 +117,7 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
@@ -125,27 +126,30 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isChargesWaiver()) {
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(loanTransaction.getPenaltyChargesPortion(currency));
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
loanTransaction.getPenaltyChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(loanTransaction.getFeeChargesPortion(currency));
feeChargesPortion = currentInstallment
.waiveFeeChargesComponent(transactionDate, loanTransaction.getFeeChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
} else if (loanTransaction.isInterestWaiver()) {
interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
} else {
// 1. pay of principal before interest.
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionAmountRemaining);
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionAmountRemaining);
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
}
@@ -1296,17 +1296,12 @@ public class Loan extends AbstractPersistable<Long> {
if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate)) {
regenerateRepaymentSchedule(loanScheduleFactory, currency);
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
} else {
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
updateLoanSummaryDerivedFields();
}
/***
* Adding a temporary Interest applied transaction (applying the
* full Interest as soon as the loan is disbursed), will have to
* re-look into this logic once loans with Interest calculation need
* to be created or more advances accrual accounting support has to
* be provided
***/
Money interestApplied = Money.of(this.getCurrency(), this.summary.getTotalInterestCharged());
final LoanTransaction interestAppliedTransaction = LoanTransaction.applyInterest(this, interestApplied, actualDisbursementDate);
this.loanTransactions.add(interestAppliedTransaction);
@@ -1315,6 +1310,13 @@ public class Loan extends AbstractPersistable<Long> {
return actualChanges;
}
private void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbursementDate) {
for (LoanRepaymentScheduleInstallment repaymentPeriod : this.repaymentScheduleInstallments) {
repaymentPeriod.updateDerivedFields(this.loanCurrency(), actualDisbursementDate);
}
}
/*
* Ability to regenerate the repayment schedule based on the loans current
* details/state.
@@ -1623,7 +1625,7 @@ public class Loan extends AbstractPersistable<Long> {
public LocalDate possibleNextRepaymentDate() {
LocalDate earliestUnpaidInstallmentDate = new LocalDate();
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
if (installment.isNotFullyCompleted()) {
if (installment.isNotFullyPaidOff()) {
earliestUnpaidInstallmentDate = installment.getDueDate();
break;
}
@@ -1649,7 +1651,7 @@ public class Loan extends AbstractPersistable<Long> {
Money possibleNextRepaymentAmount = Money.zero(currency);
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
if (installment.isNotFullyCompleted()) {
if (installment.isNotFullyPaidOff()) {
possibleNextRepaymentAmount = installment.getTotalOutstanding(currency);
break;
}
@@ -1670,7 +1672,7 @@ public class Loan extends AbstractPersistable<Long> {
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
final Money outstandingForPeriod = scheduledRepayment.getInterestOutstanding(loanCurrency());
if (scheduledRepayment.isOverdueOn(new LocalDate()) && scheduledRepayment.isNotFullyCompleted()
if (scheduledRepayment.isOverdueOn(new LocalDate()) && scheduledRepayment.isNotFullyPaidOff()
&& outstandingForPeriod.isGreaterThanZero()) {
transactionDate = scheduledRepayment.getDueDate();
possibleInterestToWaive = outstandingForPeriod;
@@ -87,13 +87,18 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
private BigDecimal penaltyChargesWaived;
@Column(name = "completed_derived", nullable = false)
private boolean completed;
private boolean obligationsMet;
@SuppressWarnings("unused")
@Temporal(TemporalType.DATE)
@Column(name = "obligations_met_on_date")
private Date obligationsMetOnDate;
protected LoanRepaymentScheduleInstallment() {
this.installmentNumber = null;
this.fromDate = null;
this.dueDate = null;
this.completed = false;
this.obligationsMet = false;
}
public LoanRepaymentScheduleInstallment(final Loan loan, final Integer installmentNumber, final LocalDate fromDate,
@@ -107,7 +112,7 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.interestCharged = defaultToNullIfZero(interest);
this.feeChargesCharged = defaultToNullIfZero(feeCharges);
this.penaltyCharges = defaultToNullIfZero(penaltyCharges);
this.completed = false;
this.obligationsMet = false;
}
private BigDecimal defaultToNullIfZero(final BigDecimal value) {
@@ -173,8 +178,8 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
}
public Money getInterestOutstanding(final MonetaryCurrency currency) {
final Money interestAccountedFor = getInterestPaid(currency).plus(getInterestWaived(currency)).plus(
getInterestWrittenOff(currency));
final Money interestAccountedFor = getInterestPaid(currency).plus(getInterestWaived(currency))
.plus(getInterestWrittenOff(currency));
return getInterestCharged(currency).minus(interestAccountedFor);
}
@@ -239,12 +244,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.loan = loan;
}
public boolean isFullyCompleted() {
return this.completed;
public boolean isObligationsMet() {
return this.obligationsMet;
}
public boolean isNotFullyCompleted() {
return !this.completed;
public boolean isNotFullyPaidOff() {
return !this.obligationsMet;
}
public boolean isPrincipalNotCompleted(final MonetaryCurrency currency) {
@@ -268,10 +273,11 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.penaltyChargesWaived = null;
this.penaltyChargesWrittenOff = null;
this.completed = false;
this.obligationsMet = false;
this.obligationsMetOnDate = null;
}
public Money payPenaltyChargesComponent(final Money transactionAmountRemaining) {
public Money payPenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money penaltyPortionOfTransaction = Money.zero(currency);
@@ -287,12 +293,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.penaltyChargesPaid = defaultToNullIfZero(this.penaltyChargesPaid);
this.completed = getTotalOutstanding(currency).isZero();
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return penaltyPortionOfTransaction;
}
public Money payFeeChargesComponent(final Money transactionAmountRemaining) {
public Money payFeeChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money feePortionOfTransaction = Money.zero(currency);
@@ -308,12 +314,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.feeChargesPaid = defaultToNullIfZero(this.feeChargesPaid);
this.completed = getTotalOutstanding(currency).isZero();
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return feePortionOfTransaction;
}
public Money payInterestComponent(final Money transactionAmountRemaining) {
public Money payInterestComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money interestPortionOfTransaction = Money.zero(currency);
@@ -329,12 +335,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.interestPaid = defaultToNullIfZero(this.interestPaid);
this.completed = getTotalOutstanding(currency).isZero();
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return interestPortionOfTransaction;
}
public Money payPrincipalComponent(final Money transactionAmountRemaining) {
public Money payPrincipalComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money principalPortionOfTransaction = Money.zero(currency);
@@ -350,12 +356,19 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.principalCompleted = defaultToNullIfZero(this.principalCompleted);
this.completed = getTotalOutstanding(currency).isZero();
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return principalPortionOfTransaction;
}
public Money waiveInterestComponent(final Money transactionAmountRemaining) {
private void checkIfRepaymentPeriodObligationsAreMet(final LocalDate transactionDate, final MonetaryCurrency currency) {
this.obligationsMet = getTotalOutstanding(currency).isZero();
if (this.obligationsMet) {
this.obligationsMetOnDate = transactionDate.toDate();
}
}
public Money waiveInterestComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money waivedInterestPortionOfTransaction = Money.zero(currency);
@@ -370,12 +383,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.interestWaived = defaultToNullIfZero(this.interestWaived);
this.completed = getTotalOutstanding(currency).isZero();
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return waivedInterestPortionOfTransaction;
}
public Money waivePenaltyChargesComponent(final Money transactionAmountRemaining) {
public Money waivePenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money waivedPenaltyChargesPortionOfTransaction = Money.zero(currency);
@@ -390,12 +403,12 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.penaltyChargesWaived = defaultToNullIfZero(this.penaltyChargesWaived);
this.completed = getTotalOutstanding(currency).isZero();
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return waivedPenaltyChargesPortionOfTransaction;
}
public Money waiveFeeChargesComponent(final Money transactionAmountRemaining) {
public Money waiveFeeChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) {
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money waivedFeeChargesPortionOfTransaction = Money.zero(currency);
@@ -410,47 +423,51 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.feeChargesWaived = defaultToNullIfZero(this.feeChargesWaived);
this.completed = getTotalOutstanding(currency).isZero();
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return waivedFeeChargesPortionOfTransaction;
}
public Money writeOffOutstandingPrincipal(final MonetaryCurrency currency) {
public Money writeOffOutstandingPrincipal(final LocalDate transactionDate, final MonetaryCurrency currency) {
final Money principalDue = getPrincipalOutstanding(currency);
this.principalWrittenOff = defaultToNullIfZero(principalDue.getAmount());
this.completed = getTotalOutstanding(currency).isZero();
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return principalDue;
}
public Money writeOffOutstandingInterest(final MonetaryCurrency currency) {
public Money writeOffOutstandingInterest(final LocalDate transactionDate, final MonetaryCurrency currency) {
final Money interestDue = getInterestOutstanding(currency);
this.interestWrittenOff = defaultToNullIfZero(interestDue.getAmount());
this.completed = getTotalOutstanding(currency).isZero();
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return interestDue;
}
public Money writeOffOutstandingFeeCharges(final MonetaryCurrency currency) {
public Money writeOffOutstandingFeeCharges(final LocalDate transactionDate, final MonetaryCurrency currency) {
final Money feeChargesOutstanding = getFeeChargesOutstanding(currency);
this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesOutstanding.getAmount());
this.completed = getTotalOutstanding(currency).isZero();
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return feeChargesOutstanding;
}
public Money writeOffOutstandingPenaltyCharges(final MonetaryCurrency currency) {
public Money writeOffOutstandingPenaltyCharges(final LocalDate transactionDate, final MonetaryCurrency currency) {
final Money penaltyChargesOutstanding = getPenaltyChargesOutstanding(currency);
this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesOutstanding.getAmount());
this.completed = getTotalOutstanding(currency).isZero();
checkIfRepaymentPeriodObligationsAreMet(transactionDate, currency);
return penaltyChargesOutstanding;
}
public boolean isOverdueOn(final LocalDate transactionDate) {
return this.getDueDate().isBefore(transactionDate);
public boolean isOverdueOn(final LocalDate date) {
return this.getDueDate().isBefore(date);
}
public void updateChargePortion(final Money feeChargesDue, final Money feeChargesWaived, final Money feeChargesWrittenOff,
@@ -462,4 +479,11 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
this.penaltyChargesWaived = defaultToNullIfZero(penaltyChargesWaived.getAmount());
this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesWrittenOff.getAmount());
}
public void updateDerivedFields(final MonetaryCurrency currency, final LocalDate actualDisbursementDate) {
if (!this.obligationsMet && getTotalOutstanding(currency).isZero()) {
this.obligationsMet = true;
this.obligationsMetOnDate = actualDisbursementDate.toDate();
}
}
}
@@ -17,107 +17,101 @@ import org.mifosplatform.organisation.monetary.domain.Money;
* For ALL types of transactions, pays off components in order of interest, then
* principal.
*
* Other formulas exist on mifos where you can choose 'Declining-Balance Interest Recalculation' which simply
* means, recalculate the interest component based on the how much principal is outstanding at a point in time;
* Other formulas exist on mifos where you can choose 'Declining-Balance
* Interest Recalculation' which simply means, recalculate the interest
* component based on the how much principal is outstanding at a point in time;
* but this isnt trying to model that option only the basic one for now.
*/
@SuppressWarnings("unused")
public class MifosStyleLoanRepaymentScheduleTransactionProcessor extends
AbstractLoanRepaymentScheduleTransactionProcessor {
public class MifosStyleLoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
@Override
protected boolean isTransactionInAdvanceOfInstallment(
final int currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> installments,
final LocalDate transactionDate, final Money transactionAmount) {
@Override
protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate, final Money transactionAmount) {
LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
return transactionDate.isBefore(currentInstallment.getDueDate());
}
return transactionDate.isBefore(currentInstallment.getDueDate());
}
/**
* For early/'in advance' repayments, pay off in the same way as on-time payments,
* interest first then principal.
*/
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance);
}
/**
* For early/'in advance' repayments, pay off in the same way as on-time
* payments, interest first then principal.
*/
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance) {
/**
* For late repayments, pay off in the same way as on-time payments,
* interest first then principal.
*/
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance);
}
return handleTransactionThatIsOnTimePaymentOfInstallment(
currentInstallment, loanTransaction,
transactionAmountUnprocessed);
}
/**
* For late repayments, pay off in the same way as on-time payments,
* interest first then principal.
*/
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
/**
* For normal on-time repayments, pays off interest first, then principal.
*/
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isChargesWaiver()) {
// penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(loanTransaction.getPenaltyChargesPortion(currency));
// transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
//
// feeChargesPortion = currentInstallment.waiveFeeChargesComponent(loanTransaction.getFeeChargesPortion(currency));
// transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
//
// zero this type of transaction and ignore it for now.
transactionAmountRemaining = Money.zero(currency);
} else if (loanTransaction.isInterestWaiver()) {
interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
} else {
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
}
return transactionAmountRemaining;
}
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed);
}
@Override
protected void onLoanOverpayment(final LoanTransaction loanTransaction,
final Money loanOverPaymentAmount) {
// TODO - KW - dont do anything with loan over-payment for now
}
/**
* For normal on-time repayments, pays off interest first, then principal.
*/
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isChargesWaiver()) {
// penaltyChargesPortion =
// currentInstallment.waivePenaltyChargesComponent(loanTransaction.getPenaltyChargesPortion(currency));
// transactionAmountRemaining =
// transactionAmountRemaining.minus(penaltyChargesPortion);
//
// feeChargesPortion =
// currentInstallment.waiveFeeChargesComponent(loanTransaction.getFeeChargesPortion(currency));
// transactionAmountRemaining =
// transactionAmountRemaining.minus(feeChargesPortion);
//
// zero this type of transaction and ignore it for now.
transactionAmountRemaining = Money.zero(currency);
} else if (loanTransaction.isInterestWaiver()) {
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
} else {
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
}
return transactionAmountRemaining;
}
@Override
protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) {
// TODO - KW - dont do anything with loan over-payment for now
}
}
@@ -16,171 +16,169 @@ import org.mifosplatform.organisation.monetary.domain.Money;
*
* From https://mifosforge.jira.com/browse/MIFOS-5636:
*
* Per RBI regulations, all interest must be paid (both current and overdue) before principal is paid.
* Per RBI regulations, all interest must be paid (both current and overdue)
* before principal is paid.
*
* For example on a loan with two installments due (one current and one overdue) of 220 each (200 principal + 20 interest):
* For example on a loan with two installments due (one current and one overdue)
* of 220 each (200 principal + 20 interest):
*
* Partial Payment of 40
* 20 Payment to interest on Installment #1 (200 principal remaining)
* 20 Payment to interest on Installment #2 (200 principal remaining)
* Partial Payment of 40 20 Payment to interest on Installment #1 (200 principal
* remaining) 20 Payment to interest on Installment #2 (200 principal remaining)
*/
public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {
/**
* For creocore, early is defined as any date before the installment due
* date
*/
@SuppressWarnings("unused")
@Override
protected boolean isTransactionInAdvanceOfInstallment(
final int currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> installments,
final LocalDate transactionDate, final Money transactionAmount) {
/**
* For creocore, early is defined as any date before the installment due
* date
*/
@SuppressWarnings("unused")
@Override
protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex,
final List<LoanRepaymentScheduleInstallment> installments, final LocalDate transactionDate, final Money transactionAmount) {
LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex);
return transactionDate.isBefore(currentInstallment.getDueDate());
}
return transactionDate.isBefore(currentInstallment.getDueDate());
}
/**
* For early/'in advance' repayments, pays off principal component only.
*/
@SuppressWarnings("unused")
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance) {
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance);
}
/**
* For early/'in advance' repayments, pays off principal component only.
*/
@SuppressWarnings("unused")
@Override
protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance) {
/**
* For late repayments, pay off in the same way as on-time payments,
* interest first then principal.
*/
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
// pay of overdue and current interest due given transaction date
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money interestWaivedPortion = Money.zero(currency);
Money feeChargesPortion = Money.zero(currency);
Money penaltyChargesPortion = Money.zero(currency);
if (loanTransaction.isInterestWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
final Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
loanTransaction.updateComponents(principalPortion, interestWaivedPortion, feeChargesPortion, penaltyChargesPortion);
} else {
LoanRepaymentScheduleInstallment currentInstallmentBasedOnTransactionDate = nearestInstallment(loanTransaction.getTransactionDate(), installments);
for (LoanRepaymentScheduleInstallment installment : installments) {
if (
installment.isInterestDue(currency) &&
(installment.isOverdueOn(loanTransaction.getTransactionDate()) || installment.getInstallmentNumber().equals(currentInstallmentBasedOnTransactionDate.getInstallmentNumber()))
) {
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
Money interestPortion = installment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
Money principalPortion = Money.zero(currency);
loanTransaction.updateComponents(principalPortion, interestPortion, Money.zero(currency), Money.zero(currency));
}
}
// With whatever is remaining, pay off principal components of installments
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isPrincipalNotCompleted(currency) && transactionAmountRemaining.isGreaterThanZero()) {
Money principalPortion = installment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
Money interestPortion = Money.zero(currency);
loanTransaction.updateComponents(principalPortion, interestPortion, Money.zero(currency), Money.zero(currency));
}
}
}
return transactionAmountRemaining;
}
return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance);
}
private LoanRepaymentScheduleInstallment nearestInstallment(final LocalDate transactionDate, List<LoanRepaymentScheduleInstallment> installments) {
LoanRepaymentScheduleInstallment nearest = installments.get(0);
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.getDueDate().isAfter(transactionDate) || installment.getDueDate().isEqual(transactionDate)) {
nearest = installment;
break;
}
}
return nearest;
}
/**
* For late repayments, pay off in the same way as on-time payments,
* interest first then principal.
*/
@Override
protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final List<LoanRepaymentScheduleInstallment> installments, final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
/**
* For normal on-time repayments, pays off interest first, then principal.
*/
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(
final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isChargesWaiver()) {
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(loanTransaction.getPenaltyChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment.waiveFeeChargesComponent(loanTransaction.getFeeChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
} else if (loanTransaction.isInterestWaiver()) {
interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
} else {
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
return transactionAmountRemaining;
}
// pay of overdue and current interest due given transaction date
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money interestWaivedPortion = Money.zero(currency);
Money feeChargesPortion = Money.zero(currency);
Money penaltyChargesPortion = Money.zero(currency);
@SuppressWarnings("unused")
@Override
protected void onLoanOverpayment(final LoanTransaction loanTransaction,
final Money loanOverPaymentAmount) {
// dont do anything for with loan over-payment
}
if (loanTransaction.isInterestWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
final Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
loanTransaction.updateComponents(principalPortion, interestWaivedPortion, feeChargesPortion, penaltyChargesPortion);
} else {
LoanRepaymentScheduleInstallment currentInstallmentBasedOnTransactionDate = nearestInstallment(
loanTransaction.getTransactionDate(), installments);
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isInterestDue(currency)
&& (installment.isOverdueOn(loanTransaction.getTransactionDate()) || installment.getInstallmentNumber().equals(
currentInstallmentBasedOnTransactionDate.getInstallmentNumber()))) {
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
Money interestPortion = installment.payInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
Money principalPortion = Money.zero(currency);
loanTransaction.updateComponents(principalPortion, interestPortion, Money.zero(currency), Money.zero(currency));
}
}
// With whatever is remaining, pay off principal components of
// installments
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.isPrincipalNotCompleted(currency) && transactionAmountRemaining.isGreaterThanZero()) {
Money principalPortion = installment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
Money interestPortion = Money.zero(currency);
loanTransaction.updateComponents(principalPortion, interestPortion, Money.zero(currency), Money.zero(currency));
}
}
}
return transactionAmountRemaining;
}
private LoanRepaymentScheduleInstallment nearestInstallment(final LocalDate transactionDate,
final List<LoanRepaymentScheduleInstallment> installments) {
LoanRepaymentScheduleInstallment nearest = installments.get(0);
for (LoanRepaymentScheduleInstallment installment : installments) {
if (installment.getDueDate().isAfter(transactionDate) || installment.getDueDate().isEqual(transactionDate)) {
nearest = installment;
break;
}
}
return nearest;
}
/**
* For normal on-time repayments, pays off interest first, then principal.
*/
@Override
protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed) {
final LocalDate transactionDate = loanTransaction.getTransactionDate();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money feeChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money penaltyChargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isChargesWaiver()) {
penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate,
loanTransaction.getPenaltyChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment
.waiveFeeChargesComponent(transactionDate, loanTransaction.getFeeChargesPortion(currency));
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
} else if (loanTransaction.isInterestWaiver()) {
interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
} else {
penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion);
feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion);
interestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
principalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
return transactionAmountRemaining;
}
@SuppressWarnings("unused")
@Override
protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) {
// dont do anything for with loan over-payment
}
}
@@ -19,6 +19,10 @@ public class LoanSchedulePeriodData {
private final Integer period;
private final LocalDate fromDate;
private final LocalDate dueDate;
@SuppressWarnings("unused")
private final LocalDate obligationsMetOnDate;
@SuppressWarnings("unused")
private final Boolean complete;
private final Integer daysInPeriod;
private final BigDecimal principalDisbursed;
@SuppressWarnings("unused")
@@ -79,8 +83,9 @@ public class LoanSchedulePeriodData {
}
public static LoanSchedulePeriodData repaymentPeriodWithPayments(@SuppressWarnings("unused") final Long loanId,
final Integer periodNumber, final LocalDate fromDate, final LocalDate dueDate, final BigDecimal principalOriginalDue,
final BigDecimal principalPaid, final BigDecimal principalWrittenOff, final BigDecimal principalOutstanding,
final Integer periodNumber, final LocalDate fromDate, final LocalDate dueDate, final LocalDate obligationsMetOnDate,
final boolean complete, final BigDecimal principalOriginalDue, final BigDecimal principalPaid,
final BigDecimal principalWrittenOff, final BigDecimal principalOutstanding,
final BigDecimal outstandingPrincipalBalanceOfLoan, final BigDecimal interestDueOnPrincipalOutstanding,
final BigDecimal interestPaid, final BigDecimal interestWaived, final BigDecimal interestWrittenOff,
final BigDecimal interestOutstanding, final BigDecimal feeChargesDue, final BigDecimal feeChargesPaid,
@@ -90,12 +95,12 @@ public class LoanSchedulePeriodData {
final BigDecimal totalPaid, final BigDecimal totalWaived, final BigDecimal totalWrittenOff, final BigDecimal totalOutstanding,
final BigDecimal totalActualCostOfLoanForPeriod) {
return new LoanSchedulePeriodData(periodNumber, fromDate, dueDate, principalOriginalDue, principalPaid, principalWrittenOff,
principalOutstanding, outstandingPrincipalBalanceOfLoan, interestDueOnPrincipalOutstanding, interestPaid, interestWaived,
interestWrittenOff, interestOutstanding, feeChargesDue, feeChargesPaid, feeChargesWaived, feeChargesWrittenOff,
feeChargesOutstanding, penaltyChargesDue, penaltyChargesPaid, penaltyChargesWaived, penaltyChargesWrittenOff,
penaltyChargesOutstanding, totalDueForPeriod, totalPaid, totalWaived, totalWrittenOff, totalOutstanding,
totalActualCostOfLoanForPeriod);
return new LoanSchedulePeriodData(periodNumber, fromDate, dueDate, obligationsMetOnDate, complete, principalOriginalDue,
principalPaid, principalWrittenOff, principalOutstanding, outstandingPrincipalBalanceOfLoan,
interestDueOnPrincipalOutstanding, interestPaid, interestWaived, interestWrittenOff, interestOutstanding, feeChargesDue,
feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, penaltyChargesDue, penaltyChargesPaid,
penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod, totalPaid, totalWaived,
totalWrittenOff, totalOutstanding, totalActualCostOfLoanForPeriod);
}
/*
@@ -107,6 +112,8 @@ public class LoanSchedulePeriodData {
this.period = periodNumber;
this.fromDate = fromDate;
this.dueDate = dueDate;
this.obligationsMetOnDate = null;
this.complete = null;
if (fromDate != null) {
this.daysInPeriod = Days.daysBetween(this.fromDate, this.dueDate).getDays();
} else {
@@ -160,6 +167,10 @@ public class LoanSchedulePeriodData {
}
}
/*
* used for repayment only period when creating an empty loan schedule for
* preview etc
*/
private LoanSchedulePeriodData(final Integer periodNumber, final LocalDate fromDate, final LocalDate dueDate,
final BigDecimal principalOriginalDue, final BigDecimal principalOutstanding,
final BigDecimal interestDueOnPrincipalOutstanding, final BigDecimal feeChargesDueForPeriod,
@@ -167,6 +178,8 @@ public class LoanSchedulePeriodData {
this.period = periodNumber;
this.fromDate = fromDate;
this.dueDate = dueDate;
this.obligationsMetOnDate = null;
this.complete = null;
if (fromDate != null) {
this.daysInPeriod = Days.daysBetween(this.fromDate, this.dueDate).getDays();
} else {
@@ -219,18 +232,21 @@ public class LoanSchedulePeriodData {
* principal, interest & charges along with what portion of each is paid.
*/
private LoanSchedulePeriodData(final Integer periodNumber, final LocalDate fromDate, final LocalDate dueDate,
final BigDecimal principalOriginalDue, final BigDecimal principalPaid, final BigDecimal principalWrittenOff,
final BigDecimal principalOutstanding, final BigDecimal principalLoanBalanceOutstanding,
final BigDecimal interestDueOnPrincipalOutstanding, final BigDecimal interestPaid, final BigDecimal interestWaived,
final BigDecimal interestWrittenOff, final BigDecimal interestOutstanding, final BigDecimal feeChargesDue,
final BigDecimal feeChargesPaid, final BigDecimal feeChargesWaived, final BigDecimal feeChargesWrittenOff,
final BigDecimal feeChargesOutstanding, final BigDecimal penaltyChargesDue, final BigDecimal penaltyChargesPaid,
final BigDecimal penaltyChargesWaived, final BigDecimal penaltyChargesWrittenOff, final BigDecimal penaltyChargesOutstanding,
final BigDecimal totalDueForPeriod, final BigDecimal totalPaid, final BigDecimal totalWaived, final BigDecimal totalWrittenOff,
final BigDecimal totalOutstanding, final BigDecimal totalActualCostOfLoanForPeriod) {
final LocalDate obligationsMetOnDate, final boolean complete, final BigDecimal principalOriginalDue,
final BigDecimal principalPaid, final BigDecimal principalWrittenOff, final BigDecimal principalOutstanding,
final BigDecimal principalLoanBalanceOutstanding, final BigDecimal interestDueOnPrincipalOutstanding,
final BigDecimal interestPaid, final BigDecimal interestWaived, final BigDecimal interestWrittenOff,
final BigDecimal interestOutstanding, final BigDecimal feeChargesDue, final BigDecimal feeChargesPaid,
final BigDecimal feeChargesWaived, final BigDecimal feeChargesWrittenOff, final BigDecimal feeChargesOutstanding,
final BigDecimal penaltyChargesDue, final BigDecimal penaltyChargesPaid, final BigDecimal penaltyChargesWaived,
final BigDecimal penaltyChargesWrittenOff, final BigDecimal penaltyChargesOutstanding, final BigDecimal totalDueForPeriod,
final BigDecimal totalPaid, final BigDecimal totalWaived, final BigDecimal totalWrittenOff, final BigDecimal totalOutstanding,
final BigDecimal totalActualCostOfLoanForPeriod) {
this.period = periodNumber;
this.fromDate = fromDate;
this.dueDate = dueDate;
this.obligationsMetOnDate = obligationsMetOnDate;
this.complete = complete;
if (fromDate != null) {
this.daysInPeriod = Days.daysBetween(this.fromDate, this.dueDate).getDays();
} else {
@@ -94,10 +94,33 @@ public final class LoanEventApiJsonValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
/**
* @param baseDataValidator
* @param element
*/
public void validateNewRepaymentTransaction(final String json) {
if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); }
final Set<String> transactionParameters = new HashSet<String>(Arrays.asList("transactionDate", "transactionAmount", "note",
"locale", "dateFormat", "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber"));
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<ApiParameterError>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction");
final JsonElement element = fromApiJsonHelper.parse(json);
final LocalDate transactionDate = fromApiJsonHelper.extractLocalDateNamed("transactionDate", element);
baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull();
final BigDecimal transactionAmount = fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element);
baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount();
final String note = fromApiJsonHelper.extractStringNamed("note", element);
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
validatePaymentDetails(baseDataValidator, element);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
}
private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) {
// Validate all string payment detail fields for max length
final Integer paymentTypeId = fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element);
@@ -672,7 +672,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
public String schema() {
return " ls.loan_id as loanId, ls.installment as period, ls.fromdate as fromDate, ls.duedate as dueDate, "
return " ls.loan_id as loanId, ls.installment as period, ls.fromdate as fromDate, ls.duedate as dueDate, ls.obligations_met_on_date as obligationsMetOnDate, ls.completed_derived as complete,"
+ " ls.principal_amount as principalDue, ls.principal_completed_derived as principalPaid, ls.principal_writtenoff_derived as principalWrittenOff, "
+ " ls.interest_amount as interestDue, ls.interest_completed_derived as interestPaid, ls.interest_waived_derived as interestWaived, ls.interest_writtenoff_derived as interestWrittenOff, "
+ " ls.fee_charges_amount as feeChargesDue, ls.fee_charges_completed_derived as feeChargesPaid, ls.fee_charges_waived_derived as feeChargesWaived, ls.fee_charges_writtenoff_derived as feeChargesWrittenOff, "
@@ -716,6 +716,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final Integer period = JdbcSupport.getInteger(rs, "period");
LocalDate fromDate = JdbcSupport.getLocalDate(rs, "fromDate");
final LocalDate dueDate = JdbcSupport.getLocalDate(rs, "dueDate");
final LocalDate obligationsMetOnDate = JdbcSupport.getLocalDate(rs, "obligationsMetOnDate");
final boolean complete = rs.getBoolean("complete");
Integer daysInPeriod = Integer.valueOf(0);
if (fromDate != null) {
@@ -788,12 +790,12 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
outstandingLoanPrincipalBalance = outstandingLoanPrincipalBalance.subtract(principalDue);
final LoanSchedulePeriodData periodData = LoanSchedulePeriodData.repaymentPeriodWithPayments(loanId, period, fromDate,
dueDate, principalDue, principalPaid, principalWrittenOff, principalOutstanding, outstandingPrincipalBalanceOfLoan,
interestExpectedDue, interestPaid, interestWaived, interestWrittenOff, interestOutstanding, feeChargesExpectedDue,
feeChargesPaid, feeChargesWaived, feeChargesWrittenOff, feeChargesOutstanding, penaltyChargesExpectedDue,
penaltyChargesPaid, penaltyChargesWaived, penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod,
totalPaidForPeriod, totalWaivedForPeriod, totalWrittenOffForPeriod, totalOutstandingForPeriod,
totalActualCostOfLoanForPeriod);
dueDate, obligationsMetOnDate, complete, principalDue, principalPaid, principalWrittenOff, principalOutstanding,
outstandingPrincipalBalanceOfLoan, interestExpectedDue, interestPaid, interestWaived, interestWrittenOff,
interestOutstanding, feeChargesExpectedDue, feeChargesPaid, feeChargesWaived, feeChargesWrittenOff,
feeChargesOutstanding, penaltyChargesExpectedDue, penaltyChargesPaid, penaltyChargesWaived,
penaltyChargesWrittenOff, penaltyChargesOutstanding, totalDueForPeriod, totalPaidForPeriod, totalWaivedForPeriod,
totalWrittenOffForPeriod, totalOutstandingForPeriod, totalActualCostOfLoanForPeriod);
periods.add(periodData);
}
@@ -297,7 +297,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
context.authenticatedUser();
this.loanEventApiJsonValidator.validateTransaction(command.json());
this.loanEventApiJsonValidator.validateNewRepaymentTransaction(command.json());
final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate");
final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount");
@@ -8,54 +8,56 @@ import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInsta
public class LoanRepaymentScheduleInstallmentBuilder {
private Loan loan = null;
private Integer installmentNumber = Integer.valueOf(1);
private LocalDate fromDate = LocalDate.now();
private LocalDate dueDate = LocalDate.now();
private MonetaryCurrency currencyDetail = new MonetaryCurrencyBuilder().build();
private Money principal = new MoneyBuilder().build();
private Money interest = new MoneyBuilder().build();
private Money feeCharges = new MoneyBuilder().build();
private Money penaltyCharges = new MoneyBuilder().build();
private boolean completed = false;
public LoanRepaymentScheduleInstallmentBuilder(MonetaryCurrency currencyDetail) {
this.currencyDetail = currencyDetail;
this.principal = new MoneyBuilder().with(currencyDetail).build();
this.interest = new MoneyBuilder().with(currencyDetail).build();
}
private final Loan loan = null;
private Integer installmentNumber = Integer.valueOf(1);
private final LocalDate fromDate = LocalDate.now();
private LocalDate dueDate = LocalDate.now();
private final LocalDate latestTransactionDate = LocalDate.now();
private MonetaryCurrency currencyDetail = new MonetaryCurrencyBuilder().build();
private Money principal = new MoneyBuilder().build();
private Money interest = new MoneyBuilder().build();
private final Money feeCharges = new MoneyBuilder().build();
private final Money penaltyCharges = new MoneyBuilder().build();
private boolean completed = false;
public LoanRepaymentScheduleInstallment build() {
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate, principal.getAmount(), interest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount());
if (completed) {
installment.payPrincipalComponent(principal);
installment.payInterestComponent(interest);
}
return installment;
}
public LoanRepaymentScheduleInstallmentBuilder(final MonetaryCurrency currencyDetail) {
this.currencyDetail = currencyDetail;
this.principal = new MoneyBuilder().with(currencyDetail).build();
this.interest = new MoneyBuilder().with(currencyDetail).build();
}
public LoanRepaymentScheduleInstallmentBuilder withPrincipal(String withPrincipal) {
this.principal = new MoneyBuilder().with(currencyDetail).with(withPrincipal).build();
return this;
}
public LoanRepaymentScheduleInstallmentBuilder withInterest(String withInterest) {
this.interest = new MoneyBuilder().with(currencyDetail).with(withInterest).build();
return this;
}
public LoanRepaymentScheduleInstallment build() {
LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan, installmentNumber, fromDate, dueDate,
principal.getAmount(), interest.getAmount(), feeCharges.getAmount(), penaltyCharges.getAmount());
if (completed) {
installment.payPrincipalComponent(latestTransactionDate, principal);
installment.payInterestComponent(latestTransactionDate, interest);
}
return installment;
}
public LoanRepaymentScheduleInstallmentBuilder withDueDate(LocalDate withDueDate) {
this.dueDate = withDueDate;
return this;
}
public LoanRepaymentScheduleInstallmentBuilder withPrincipal(final String withPrincipal) {
this.principal = new MoneyBuilder().with(currencyDetail).with(withPrincipal).build();
return this;
}
public LoanRepaymentScheduleInstallmentBuilder completed() {
this.completed = true;
return this;
}
public LoanRepaymentScheduleInstallmentBuilder withInterest(final String withInterest) {
this.interest = new MoneyBuilder().with(currencyDetail).with(withInterest).build();
return this;
}
public LoanRepaymentScheduleInstallmentBuilder withInstallmentNumber(final int withInstallmentNumber) {
this.installmentNumber = withInstallmentNumber;
return this;
}
}
public LoanRepaymentScheduleInstallmentBuilder withDueDate(final LocalDate withDueDate) {
this.dueDate = withDueDate;
return this;
}
public LoanRepaymentScheduleInstallmentBuilder completed() {
this.completed = true;
return this;
}
public LoanRepaymentScheduleInstallmentBuilder withInstallmentNumber(final int withInstallmentNumber) {
this.installmentNumber = withInstallmentNumber;
return this;
}
}