mifosx-61: provide ability to close loans as reschedule or obligations met if loan is within defined tolerance.

This commit is contained in:
Keith Woodlock
2012-10-10 15:02:41 +01:00
parent e60f1798aa
commit c4578672fa
22 changed files with 231 additions and 147 deletions
@@ -0,0 +1 @@
alter table m_loan_transaction drop column interest_waived_derived;
@@ -187,7 +187,7 @@ public class LoansApiResource {
}
boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
LoanBasicDetailsData loanBasicDetails = this.loanReadPlatformService.retrieveLoanAccountDetails(loanId);
final LoanBasicDetailsData loanBasicDetails = this.loanReadPlatformService.retrieveLoanAccountDetails(loanId);
int loanRepaymentsCount = 0;
Collection<LoanTransactionNewData> loanRepayments = null;
@@ -223,7 +223,9 @@ public class LoansApiResource {
MoneyData totalOutstandingMoney = MoneyData.of(loanBasicDetails.getCurrency(), repaymentSchedule.totalOutstanding());
boolean isWaiveAllowed = totalOutstandingMoney.isGreaterThanZero() && (tolerance.isGreaterThan(totalOutstandingMoney) || tolerance.isEqualTo(totalOutstandingMoney));
permissions = this.loanReadPlatformService.retrieveLoanPermissions(loanBasicDetails, isWaiveAllowed, loanRepaymentsCount);
if (responseParameters.contains("permissions")) {
permissions = this.loanReadPlatformService.retrieveLoanPermissions(loanBasicDetails, isWaiveAllowed, loanRepaymentsCount);
}
convenienceDataRequired = true;
}
@@ -418,8 +420,10 @@ public class LoansApiResource {
identifier = this.loanWritePlatformService.waiveInterestOnLoan(command);
} else if (is(commandParam, "writeoff")) {
identifier = this.loanWritePlatformService.writeOff(command);
} else if (is(commandParam, "closeasrescheduled")) {
} else if (is(commandParam, "close-rescheduled")) {
identifier = this.loanWritePlatformService.closeAsRescheduled(command);
} else if (is(commandParam, "close")) {
identifier = this.loanWritePlatformService.closeLoan(command);
}
if (identifier == null) {
@@ -474,7 +478,7 @@ public class LoansApiResource {
final LoanTransactionNewData newTransactionData = this.loanReadPlatformService.retrieveNewClosureDetails();
return this.apiJsonSerializerService.serializeLoanTransactionDataToJson(prettyPrint, responseParameters, newTransactionData);
} else if (is(commandParam, "closeasrescheduled")) {
} else if (is(commandParam, "close-rescheduled")) {
final Set<String> typicalResponseParameters = new HashSet<String>(Arrays.asList("id", "type", "date", "currency", "amount"));
Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
@@ -485,7 +489,18 @@ public class LoansApiResource {
final LoanTransactionNewData newTransactionData = this.loanReadPlatformService.retrieveNewClosureDetails();
return this.apiJsonSerializerService.serializeLoanTransactionDataToJson(prettyPrint, responseParameters, newTransactionData);
} else {
} else if (is(commandParam, "close")) {
final Set<String> typicalResponseParameters = new HashSet<String>(Arrays.asList("id", "type", "date", "currency", "amount"));
final Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
if (responseParameters.isEmpty()) {
responseParameters.addAll(typicalResponseParameters);
}
boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
final LoanTransactionNewData newTransactionData = this.loanReadPlatformService.retrieveNewClosureDetails();
return this.apiJsonSerializerService.serializeLoanTransactionDataToJson(prettyPrint, responseParameters, newTransactionData);
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
}
@@ -1,7 +1,15 @@
package org.mifosng.platform.api.data;
/**
* Immutable data object containing information on permissions allowed on a given loan.
*/
@SuppressWarnings("unused")
public class LoanPermissionData {
// permission for actions on loan
private final boolean closeLoanAllowed;
private final boolean closeLoanAsRescheduledAllowed;
private final boolean addLoanChargeAllowed;
private final boolean waiveAllowed;
private final boolean makeRepaymentAllowed;
private final boolean rejectAllowed;
@@ -9,15 +17,30 @@ public class LoanPermissionData {
private final boolean undoApprovalAllowed;
private final boolean disbursalAllowed;
private final boolean undoDisbursalAllowed;
private final boolean pendingApproval;
private final boolean waitingForDisbursal;
private final boolean anyActionOnLoanAllowed;
public LoanPermissionData(final boolean waiveAllowed,
final boolean makeRepaymentAllowed, final boolean rejectAllowed,
final boolean withdrawnByApplicantAllowed, final boolean undoApprovalAllowed,
final boolean undoDisbursalAllowed, final boolean disbursalAllowed, final boolean pendingApproval, final boolean waitingForDisbursal) {
// status of loan
private final boolean pendingApproval;
private final boolean waitingForDisbursal;
private final boolean closedObligationsMet;
public LoanPermissionData(
final boolean closeLoanAllowed,
final boolean closeLoanAsRescheduledAllowed,
final boolean addLoanChargeAllowed,
final boolean waiveAllowed,
final boolean makeRepaymentAllowed,
final boolean rejectAllowed,
final boolean withdrawnByApplicantAllowed,
final boolean undoApprovalAllowed,
final boolean undoDisbursalAllowed,
final boolean disbursalAllowed,
final boolean pendingApproval,
final boolean waitingForDisbursal,
final boolean closedObligationsMet) {
this.closeLoanAllowed = closeLoanAllowed;
this.closeLoanAsRescheduledAllowed = closeLoanAsRescheduledAllowed;
this.addLoanChargeAllowed = addLoanChargeAllowed;
this.waiveAllowed = waiveAllowed;
this.makeRepaymentAllowed = makeRepaymentAllowed;
this.rejectAllowed = rejectAllowed;
@@ -27,46 +50,7 @@ public class LoanPermissionData {
this.disbursalAllowed = disbursalAllowed;
this.pendingApproval = pendingApproval;
this.waitingForDisbursal = waitingForDisbursal;
this.anyActionOnLoanAllowed = isRejectAllowed() || isWithdrawnByApplicantAllowed() || isPendingApproval() || isUndoDisbursalAllowed() || isMakeRepaymentAllowed();
}
public boolean isWaiveAllowed() {
return waiveAllowed;
}
public boolean isMakeRepaymentAllowed() {
return makeRepaymentAllowed;
}
public boolean isRejectAllowed() {
return rejectAllowed;
}
public boolean isWithdrawnByApplicantAllowed() {
return withdrawnByApplicantAllowed;
}
public boolean isUndoApprovalAllowed() {
return undoApprovalAllowed;
}
public boolean isDisbursalAllowed() {
return disbursalAllowed;
}
public boolean isUndoDisbursalAllowed() {
return undoDisbursalAllowed;
}
public boolean isPendingApproval() {
return pendingApproval;
}
public boolean isWaitingForDisbursal() {
return waitingForDisbursal;
}
public boolean isAnyActionOnLoanAllowed() {
return anyActionOnLoanAllowed;
this.closedObligationsMet = closedObligationsMet;
this.anyActionOnLoanAllowed = closeLoanAllowed || closeLoanAsRescheduledAllowed || rejectAllowed || withdrawnByApplicantAllowed || pendingApproval || undoDisbursalAllowed || makeRepaymentAllowed;
}
}
@@ -11,7 +11,6 @@ import org.mifosng.platform.api.commands.DepositAccountWithdrawalCommand;
import org.mifosng.platform.api.commands.DepositProductCommand;
import org.mifosng.platform.api.commands.DepositStateTransitionApprovalCommand;
import org.mifosng.platform.api.commands.DepositStateTransitionCommand;
import org.mifosng.platform.api.commands.DocumentCommand;
import org.mifosng.platform.api.commands.FundCommand;
import org.mifosng.platform.api.commands.GroupCommand;
import org.mifosng.platform.api.commands.LoanApplicationCommand;
@@ -88,7 +88,6 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments) {
final Money interestWaivedPortion = Money.zero(currency);
Money principalPortion = Money.zero(currency);
Money interestPortion = Money.zero(currency);
Money chargesPortion = Money.zero(currency);
@@ -102,7 +101,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
}
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, chargesPortion);
}
// abstract interface
@@ -73,8 +73,7 @@ public class AdhikarLoanRepaymentScheduleTransactionProcessor extends AbstractLo
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
final Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
final Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
loanTransaction.updateComponents(principalPortion, interestWaivedPortion, chargesPortion);
} else {
LoanRepaymentScheduleInstallment currentInstallmentBasedOnTransactionDate = nearestInstallment(loanTransaction.getTransactionDate(), installments);
@@ -88,7 +87,7 @@ public class AdhikarLoanRepaymentScheduleTransactionProcessor extends AbstractLo
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
Money principalPortion = Money.zero(currency);
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, Money.zero(transactionAmountRemaining.getCurrency()));
loanTransaction.updateComponents(principalPortion, interestPortion, Money.zero(transactionAmountRemaining.getCurrency()));
}
}
@@ -99,7 +98,7 @@ public class AdhikarLoanRepaymentScheduleTransactionProcessor extends AbstractLo
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
Money interestPortion = Money.zero(currency);
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, Money.zero(transactionAmountRemaining.getCurrency()));
loanTransaction.updateComponents(principalPortion, interestPortion, Money.zero(transactionAmountRemaining.getCurrency()));
}
}
}
@@ -130,14 +129,13 @@ public class AdhikarLoanRepaymentScheduleTransactionProcessor extends AbstractLo
final Money transactionAmountUnprocessed) {
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money interestWaivedPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money chargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isInterestWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
} else {
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
@@ -147,7 +145,7 @@ public class AdhikarLoanRepaymentScheduleTransactionProcessor extends AbstractLo
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
loanTransaction.updateComponents(principalPortion, interestPortion, chargesPortion);
return transactionAmountRemaining;
}
@@ -79,14 +79,13 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends
final Money transactionAmountUnprocessed) {
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money interestWaivedPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money chargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isInterestWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
} else {
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
@@ -96,7 +95,7 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
loanTransaction.updateComponents(principalPortion, interestPortion, chargesPortion);
return transactionAmountRemaining;
}
@@ -11,6 +11,7 @@ import org.mifosng.platform.currency.domain.Money;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Deprecated
public class DailyEquivalentDecliningBalanceInterestRebateCalculator implements
InterestRebateCalculator {
@@ -9,8 +9,8 @@ import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
// FIXME - KW - this might be deprecated now
@SuppressWarnings("unused")
@Deprecated
public class DailyEquivalentFlatInterestRebateCalculator implements
InterestRebateCalculator {
@@ -60,7 +60,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
}
break;
case REPAID_IN_FULL:
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) {
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE, LoanStatus.OVERPAID)) {
newState = stateOf(LoanStatus.CLOSED_OBLIGATIONS_MET, allowedLoanStatuses);
}
break;
@@ -68,23 +68,23 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
final MonetaryCurrency currency = paymentInAdvance.getCurrency();
Money transactionAmountRemaining = paymentInAdvance;
Money interestWaivedPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money chargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isInterestWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
} else {
if (currentInstallment.isPrincipalNotCompleted(currency)) {
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
if (currentInstallment.isPrincipalCompleted(currency)) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(currentInstallment.getInterest(currency));
// FIXME - KW - if auto waiving interest need to create another transaction to handle this.
currentInstallment.waiveInterestComponent(currentInstallment.getInterest(currency));
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
loanTransaction.updateComponents(principalPortion, interestPortion, chargesPortion);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
@@ -96,7 +96,7 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
loanTransaction.updateComponents(principalPortion, interestPortion, chargesPortion);
return transactionAmountRemaining;
}
@@ -111,14 +111,13 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money interestWaivedPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money chargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isInterestWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
} else {
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
@@ -128,7 +127,7 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
loanTransaction.updateComponents(principalPortion, interestPortion, chargesPortion);
return transactionAmountRemaining;
}
@@ -6,6 +6,7 @@ import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.Money;
@Deprecated
public interface InterestRebateCalculator {
Money calculate(
@@ -25,7 +25,6 @@ import org.apache.commons.lang.ObjectUtils;
import org.apache.commons.lang.StringUtils;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.mifosng.platform.api.LoanScheduleData;
import org.mifosng.platform.api.commands.LoanApplicationCommand;
@@ -533,7 +532,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
LoanTransaction chargesPayment = LoanTransaction.repaymentAtDisbursement(getTotalChargesDueAtDisbursement(), disbursedOn);
Money zero = Money.zero(getCurrency());
chargesPayment.updateComponents(zero, zero, zero, getTotalChargesDueAtDisbursement());
chargesPayment.updateComponents(zero, zero, getTotalChargesDueAtDisbursement());
chargesPayment.updateLoan(this);
this.loanTransactions.add(chargesPayment);
@@ -871,10 +870,14 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
private boolean isOverPaid() {
return getTotalOverpayment().isGreaterThanZero();
}
private Money getTotalOverpayment() {
Money totalPaidInRepayments = getTotalPaidInRepayments();
final Money totalPaidInRepayments = getTotalPaidInRepayments();
MonetaryCurrency currency = loanCurrency();
final MonetaryCurrency currency = loanCurrency();
Money cumulativeOriginalExpectedTotal = Money.zero(currency);
Money cumulativeTotalPaidOnInstallments = Money.zero(currency);
@@ -886,7 +889,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
cumulativeTotalWaivedOnInstallments = cumulativeTotalWaivedOnInstallments.plus(scheduledRepayment.getInterestWaived(currency));
}
return totalPaidInRepayments.isGreaterThan(cumulativeTotalPaidOnInstallments);
return totalPaidInRepayments.minus(cumulativeTotalPaidOnInstallments);
}
private MonetaryCurrency loanCurrency() {
@@ -932,13 +935,76 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
return loanTransaction;
}
// FIXME - KW - finish off reschedule/restructure capability for loans.
public void reschedule(
final DateTime rescheduledOn,
public LoanTransaction close(
final LocalDate closureDate,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_RESCHEDULE, LoanStatus.fromInt(this.loanStatus));
if (closureDate.isBefore(this.getDisbursementDate())) {
final String errorMessage = "The date on which a loan is closed cannot be before the loan disbursement date: " + getDisbursementDate().toString();
throw new InvalidLoanStateTransitionException("close","cannot.be.before.submittal.date", errorMessage, closureDate, getDisbursementDate());
}
if (closureDate.isAfter(new LocalDate())) {
final String errorMessage = "The date on which a loan is closed cannot be in the future.";
throw new InvalidLoanStateTransitionException("close", "cannot.be.a.future.date", errorMessage, closureDate);
}
LoanTransaction loanTransaction = null;
if (isOpen()) {
// 1. check total outstanding
final Money outstanding = getTotalOutstanding();
if (outstanding.isGreaterThanZero() && getInArrearsTolerance().isGreaterThanOrEqualTo(outstanding)) {
updateLoanForClosure(closureDate, loanLifecycleStateMachine);
loanTransaction = LoanTransaction.writeoff(this, closureDate);
boolean isLastTransaction = isChronologicallyLatestTransaction(loanTransaction, loanTransactions);
if (!isLastTransaction) {
final String errorMessage = "The closing date of the loan must be on or after latest transaction date.";
throw new InvalidLoanStateTransitionException("close.loan","must.occur.on.or.after.latest.transaction.date", errorMessage, closureDate);
}
this.loanTransactions.add(loanTransaction);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor(this.transactionProcessingStrategy);
loanRepaymentScheduleTransactionProcessor.handleWriteOff(loanTransaction, loanCurrency(), this.repaymentScheduleInstallments);
} else if (outstanding.isGreaterThanZero()) {
final String errorMessage = "A loan with money outstanding cannot be closed";
throw new InvalidLoanStateTransitionException("close", "loan.has.money.outstanding", errorMessage, outstanding.toString());
}
}
if (isOverPaid()) {
final Money totalLoanOverpayment = getTotalOverpayment();
// FIXME - kw - use overpaymentTolerance setting when in place on loan product settings.
if (totalLoanOverpayment.isGreaterThanZero() && getInArrearsTolerance().isGreaterThanOrEqualTo(totalLoanOverpayment)) {
// TODO - technically should set somewhere that this loan has 'overpaid' amount
updateLoanForClosure(closureDate, loanLifecycleStateMachine);
} else if (totalLoanOverpayment.isGreaterThanZero()) {
final String errorMessage = "The loan is marked as 'Overpaid' and cannot be moved to 'Closed (obligations met).";
throw new InvalidLoanStateTransitionException("close", "loan.is.overpaid", errorMessage, totalLoanOverpayment.toString());
}
}
return loanTransaction;
}
private void updateLoanForClosure(final LocalDate closureDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
this.closedOnDate = closureDate.toDate();
}
/**
* Behaviour added to comply with capability of previous mifos product to support easier transition to mifosx platform.
*/
public void closeAsMarkedForReschedule(
final LocalDate rescheduledOn,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
final LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_RESCHEDULE, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
this.closedOnDate = rescheduledOn.toDate();
@@ -948,15 +1014,12 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
if (rescheduledOnLocalDate.isBefore(this.getDisbursementDate())) {
final String errorMessage = "The date on which a loan is rescheduled cannot be before the loan disbursement date: "
+ getDisbursementDate().toString();
throw new InvalidLoanStateTransitionException("writeoff",
"cannot.be.before.submittal.date", errorMessage,
rescheduledOnLocalDate, getDisbursementDate());
throw new InvalidLoanStateTransitionException("close.reschedule","cannot.be.before.submittal.date", errorMessage, rescheduledOnLocalDate, getDisbursementDate());
}
if (rescheduledOnLocalDate.isAfter(new LocalDate())) {
final String errorMessage = "The date on which a loan is rescheduled cannot be in the future.";
throw new InvalidLoanStateTransitionException("writeoff",
"cannot.be.a.future.date", errorMessage,
rescheduledOnLocalDate);
throw new InvalidLoanStateTransitionException("close.reschedule", "cannot.be.a.future.date", errorMessage, rescheduledOnLocalDate);
}
}
@@ -1219,6 +1282,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
return regenerationRequired;
}
@Deprecated
public LoanPayoffSummary getPayoffSummaryOn(
final LocalDate projectedPayoffDate) {
@@ -1246,6 +1310,10 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
rebateGivenOnProjectedPayoffDate);
}
/*
* FIXME - KW - change what outstanding balance on loan can be accessed. This total outstanding method is wrong and
*/
@Deprecated
public Money getTotalOutstanding() {
return getTotalPrincipalOnLoan().plus(getTotalInterestOnLoan()).minus(getTotalPaidInRepayments());
}
@@ -1262,6 +1330,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
return cumulativePaid;
}
@Deprecated
public Money calculateRebateWhenPaidInFullOn(final LocalDate paidInFullDate) {
Money loanPrincipal = this.loanRepaymentScheduleDetail.getPrincipal();
@@ -1290,8 +1359,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
Money cumulativeInterest = Money.zero(loanCurrency());
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment
.getInterest(loanCurrency()));
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment.getInterest(loanCurrency()));
}
return cumulativeInterest;
@@ -4,6 +4,7 @@ import org.joda.time.Days;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.Money;
@Deprecated
public class LoanPayoffSummary {
private final Long reference;
@@ -43,9 +43,6 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
@Column(name = "interest_portion_derived", scale = 6, precision = 19, nullable = false)
private BigDecimal interestPortion = BigDecimal.ZERO;
@Column(name = "interest_waived_derived", scale = 6, precision = 19)
private BigDecimal interestWaivedPortion = BigDecimal.ZERO;
@Column(name = "charges_portion_derived", scale = 6, precision = 19, nullable = false)
private BigDecimal chargesPortion = BigDecimal.ZERO;
@@ -187,16 +184,15 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
*
* This accumulates the values passed to the already existent values for each of the portions.
*/
public void updateComponents(final Money principal, final Money interest, final Money interestWaivedPortion, final Money charges) {
public void updateComponents(final Money principal, final Money interest, final Money charges) {
MonetaryCurrency currency = principal.getCurrency();
this.principalPortion = getPrincipalPortion(currency).plus(principal).getAmount();
this.interestPortion = getInterestPortion(currency).plus(interest).getAmount();
this.interestWaivedPortion = getInterestWaivedPortion(currency).plus(interestWaivedPortion).getAmount();
this.chargesPortion = getChargesPortion(currency).plus(charges).getAmount();
}
public void updateComponentsAndTotal(final Money principal, final Money interest, final Money interestWaivedPortion, final Money charges) {
updateComponents(principal, interest, interestWaivedPortion, charges);
public void updateComponentsAndTotal(final Money principal, final Money interest, final Money charges) {
updateComponents(principal, interest, charges);
final MonetaryCurrency currency = principal.getCurrency();
this.amount = getPrincipalPortion(currency).plus(getInterestPortion(currency)).plus(this.chargesPortion).getAmount();
@@ -210,13 +206,6 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
return Money.of(currency, interestPortion);
}
public Money getInterestWaivedPortion(final MonetaryCurrency currency) {
if (this.interestWaivedPortion == null) {
this.interestWaivedPortion = BigDecimal.ZERO;
}
return Money.of(currency, interestWaivedPortion);
}
public Money getChargesPortion(final MonetaryCurrency currency) {
if (this.chargesPortion == null) {
this.chargesPortion = BigDecimal.ZERO;
@@ -227,7 +216,6 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
public void resetDerivedComponents() {
this.principalPortion = null;
this.interestPortion = null;
this.interestWaivedPortion = null;
this.chargesPortion = null;
}
}
@@ -70,14 +70,13 @@ public class MifosStyleLoanRepaymentScheduleTransactionProcessor extends
final Money transactionAmountUnprocessed) {
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money interestWaivedPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money chargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
if (loanTransaction.isInterestWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
interestPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
} else {
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
@@ -87,7 +86,7 @@ public class MifosStyleLoanRepaymentScheduleTransactionProcessor extends
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
loanTransaction.updateComponents(principalPortion, interestPortion, chargesPortion);
return transactionAmountRemaining;
}
@@ -77,6 +77,10 @@ public class CalculationPlatformServiceImpl implements CalculationPlatformServic
command.getInterestChargedFromDate(), loanCharges);
}
/**
* @deprecated functionality from early prototype work with creocore
*/
@Deprecated
@Override
public LoanPayoffReadModel calculatePayoffOn(final Long loanId, final LocalDate payoffDate) {
@@ -185,28 +185,34 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final boolean isWaiverAllowed,
final int repaymentAndWaiveCount) {
boolean pendingApproval = (loanBasicDetails.getStatus().getId().equals(100L));
boolean waitingForDisbursal = (loanBasicDetails.getStatus().getId().equals(200L));
boolean isActive = (loanBasicDetails.getStatus().getId().equals(300L));
final boolean pendingApproval = (loanBasicDetails.getStatus().getId().equals(100L));
final boolean waitingForDisbursal = (loanBasicDetails.getStatus().getId().equals(200L));
final boolean isActive = (loanBasicDetails.getStatus().getId().equals(300L));
final boolean closedObligationsMet = (loanBasicDetails.getStatus().getId().equals(600L));
final boolean isOverpaid = (loanBasicDetails.getStatus().getId().equals(700L));
final boolean addLoanChargeAllowed = !closedObligationsMet || !isOverpaid;
boolean waiveAllowed = isWaiverAllowed && isActive;
boolean makeRepaymentAllowed = isActive;
boolean rejectAllowed = pendingApproval;
boolean withdrawnByApplicantAllowed = waitingForDisbursal
final boolean waiveAllowed = isWaiverAllowed && isActive;
final boolean makeRepaymentAllowed = isActive;
final boolean closeLoanAllowed = !closedObligationsMet;
final boolean closeLoanAsRescheduledAllowed = !closedObligationsMet && !isOverpaid;
final boolean rejectAllowed = pendingApproval;
final boolean withdrawnByApplicantAllowed = waitingForDisbursal
|| pendingApproval;
boolean undoApprovalAllowed = waitingForDisbursal;
final boolean undoApprovalAllowed = waitingForDisbursal;
boolean undoDisbursalAllowed = isActive
final boolean undoDisbursalAllowed = isActive
&& (repaymentAndWaiveCount == 0);
boolean disbursalAllowed = waitingForDisbursal;
final boolean disbursalAllowed = waitingForDisbursal;
return new LoanPermissionData(waiveAllowed, makeRepaymentAllowed,
return new LoanPermissionData(closeLoanAllowed, closeLoanAsRescheduledAllowed,
addLoanChargeAllowed, waiveAllowed, makeRepaymentAllowed,
rejectAllowed, withdrawnByApplicantAllowed,
undoApprovalAllowed, undoDisbursalAllowed, disbursalAllowed,
pendingApproval, waitingForDisbursal);
pendingApproval, waitingForDisbursal, closedObligationsMet);
}
@Override
@@ -49,6 +49,9 @@ public interface LoanWritePlatformService {
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier writeOff(LoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier closeLoan(LoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier closeAsRescheduled(LoanTransactionCommand command);
@@ -568,6 +568,35 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
return new EntityIdentifier(loan.getId());
}
@Transactional
@Override
public EntityIdentifier closeLoan(final LoanTransactionCommand command) {
context.authenticatedUser();
final LoanTransactionCommandValidator validator = new LoanTransactionCommandValidator(command);
validator.validateNonMonetaryTransaction();
final Loan loan = this.loanRepository.findOne(command.getLoanId());
if (loan == null) {
throw new LoanNotFoundException(command.getLoanId());
}
final LoanTransaction possibleClosingTransaction = loan.close(command.getTransactionDate(), defaultLoanLifecycleStateMachine());
if (possibleClosingTransaction != null) {
this.loanTransactionRepository.save(possibleClosingTransaction);
}
this.loanRepository.save(loan);
final String noteText = command.getNote();
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
return new EntityIdentifier(loan.getId());
}
@Transactional
@Override
public EntityIdentifier closeAsRescheduled(final LoanTransactionCommand command) {
@@ -581,15 +610,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
throw new LoanNotFoundException(command.getLoanId());
}
// FIXME - kw - HANDLE closed and rescheduled case.
final LoanTransaction writeoff = loan.closeAsWrittenOff(command.getTransactionDate(), defaultLoanLifecycleStateMachine());
loan.closeAsMarkedForReschedule(command.getTransactionDate(), defaultLoanLifecycleStateMachine());
this.loanTransactionRepository.save(writeoff);
this.loanRepository.save(loan);
final String noteText = command.getNote();
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanTransactionNote(loan, writeoff, noteText);
final Note note = Note.loanNote(loan, noteText);
this.noteRepository.save(note);
}
@@ -49,7 +49,7 @@ public class AdhikarLoanRepaymentScheduleTransactionProcessorTest {
LoanRepaymentScheduleInstallment firstInstallment = this.installments.get(0);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("0.00"), interestOf("100.00"), interestWaivedOf("0.00"), firstInstallment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("0.00"), interestOf("100.00"), interestWaivedOf("0.00"), onTimePartialRepayment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("0.00"), interestOf("100.00"), onTimePartialRepayment, usDollars);
}
/**
@@ -77,7 +77,7 @@ public class AdhikarLoanRepaymentScheduleTransactionProcessorTest {
LoanRepaymentScheduleInstallment thirdInstallment = this.installments.get(2);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("0.00"), interestOf("0.00"), interestWaivedOf("0.00"), thirdInstallment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("0.00"), interestOf("300.00"), interestWaivedOf("0.00"), lateRepayment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("0.00"), interestOf("300.00"), lateRepayment, usDollars);
}
/**
@@ -105,13 +105,12 @@ public class AdhikarLoanRepaymentScheduleTransactionProcessorTest {
LoanRepaymentScheduleInstallment thirdInstallment = this.installments.get(2);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("0.00"), interestOf("0.00"), interestWaivedOf("0.00"), thirdInstallment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("500.00"), interestOf("400.00"), interestWaivedOf("0.00"), lateRepayment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("500.00"), interestOf("400.00"), lateRepayment, usDollars);
}
private void assertThatLoanTransactionHasDerivedFieldsOf(
final String principal,
final String interest,
final String interestWaived,
final LoanTransaction transaction, final MonetaryCurrency currency) {
Money expectedPrincipalPortion = new MoneyBuilder().with(currency).with(principal).build();
@@ -119,9 +118,6 @@ public class AdhikarLoanRepaymentScheduleTransactionProcessorTest {
Money expectedInterestPortion = new MoneyBuilder().with(currency).with(interest).build();
assertThat("Interest portion of transaction not as expected: ", transaction.getInterestPortion(currency).toString(), is(expectedInterestPortion.toString()));
Money expectedInterestWaivedPortion = new MoneyBuilder().with(currency).with(interestWaived).build();
assertThat("Interest waived portion of transaction not as expected: ",transaction.getInterestWaivedPortion(currency).toString(), is(expectedInterestWaivedPortion.toString()));
}
private void assertThatLoanInstallmentHasDerivedFieldsOf(
@@ -278,7 +278,7 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest {
LoanRepaymentScheduleInstallment thirdInstallment = this.installments.get(2);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("800.00"), interestOf("0.00"), interestWaivedOf("0.00"), thirdInstallment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("2800.00"), interestOf("200.00"), interestWaivedOf("200.00"), onTimePartialPayment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("2800.00"), interestOf("200.00"), onTimePartialPayment, usDollars);
}
/**
@@ -353,7 +353,7 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest {
LoanRepaymentScheduleInstallment thirdInstallment = this.installments.get(2);
assertThatLoanInstallmentHasDerivedFieldsOf(principalOf("1000.00"), interestOf("200.00"), interestWaivedOf("0.00"), thirdInstallment, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("3000.00"), interestOf("600.00"), interestWaivedOf("0.00"), lateRepaymentTransaction, usDollars);
assertThatLoanTransactionHasDerivedFieldsOf(principalOf("3000.00"), interestOf("600.00"), lateRepaymentTransaction, usDollars);
}
@@ -379,7 +379,6 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest {
private void assertThatLoanTransactionHasDerivedFieldsOf(
final String principal,
final String interest,
final String interestWaived,
final LoanTransaction transaction, final MonetaryCurrency currency) {
Money expectedPrincipalPortion = new MoneyBuilder().with(currency).with(principal).build();
@@ -387,9 +386,6 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessorTest {
Money expectedInterestPortion = new MoneyBuilder().with(currency).with(interest).build();
assertThat("Interest portion of transaction not as expected: ", transaction.getInterestPortion(currency).toString(), is(expectedInterestPortion.toString()));
Money expectedInterestWaivedPortion = new MoneyBuilder().with(currency).with(interestWaived).build();
assertThat("Interest waived portion of transaction not as expected: ",transaction.getInterestWaivedPortion(currency).toString(), is(expectedInterestWaivedPortion.toString()));
}
private void assertThatLoanInstallmentHasDerivedFieldsOf(