creocore: change waive functionality to support waive of interest only.

This commit is contained in:
Keith Woodlock
2012-10-04 16:08:31 +01:00
parent cf865a28d9
commit 0a22ab3e41
18 changed files with 377 additions and 235 deletions
@@ -40,6 +40,7 @@ import org.mifosng.platform.api.data.LoanPermissionData;
import org.mifosng.platform.api.data.LoanProductLookup;
import org.mifosng.platform.api.data.LoanRepaymentTransactionData;
import org.mifosng.platform.api.data.LoanTransactionData;
import org.mifosng.platform.api.data.LoanTransactionNewData;
import org.mifosng.platform.api.data.MoneyData;
import org.mifosng.platform.api.data.StaffData;
import org.mifosng.platform.api.data.TransactionProcessingStrategyData;
@@ -414,9 +415,9 @@ public class LoansApiResource {
EntityIdentifier identifier = this.loanWritePlatformService
.makeLoanRepayment(command);
response = Response.ok().entity(identifier).build();
} else if (is(commandParam, "waiver")) {
} else if (is(commandParam, "waiveinterest")) {
EntityIdentifier identifier = this.loanWritePlatformService
.waiveLoanAmount(command);
.waiveInterestOnLoan(command);
response = Response.ok().entity(identifier).build();
}
@@ -436,26 +437,34 @@ public class LoansApiResource {
@QueryParam("command") final String commandParam,
@Context final UriInfo uriInfo) {
Set<String> typicalResponseParameters = new HashSet<String>(
Arrays.asList("id", "transactionType", "date", "principal", "interest", "total", "totalWaived", "overpaid")
);
Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
if (responseParameters.isEmpty()) {
responseParameters.addAll(typicalResponseParameters);
}
boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
LoanTransactionData transactionData = null;
if (is(commandParam, "repayment")) {
Set<String> typicalResponseParameters = new HashSet<String>(
Arrays.asList("id", "transactionType", "date", "principal", "interest", "total", "totalWaived", "overpaid")
);
Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
if (responseParameters.isEmpty()) {
responseParameters.addAll(typicalResponseParameters);
}
boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
transactionData = this.loanReadPlatformService.retrieveNewLoanRepaymentDetails(loanId);
} else if (is(commandParam, "waiver")) {
transactionData = this.loanReadPlatformService.retrieveNewLoanWaiverDetails(loanId);
return this.apiJsonSerializerService.serializeLoanTransactionDataToJson(prettyPrint, responseParameters, transactionData);
} else if (is(commandParam, "waiveinterest")) {
final Set<String> typicalResponseParameters = new HashSet<String>(Arrays.asList("id", "type", "date", "currency", "amount"));
Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
if (responseParameters.isEmpty()) {
responseParameters.addAll(typicalResponseParameters);
}
boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
LoanTransactionNewData newTransactionData = this.loanReadPlatformService.retrieveNewLoanWaiveInterestDetails(loanId);
return this.apiJsonSerializerService.serializeLoanTransactionDataToJson(prettyPrint, responseParameters, newTransactionData);
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
return this.apiJsonSerializerService.serializeLoanTransactionDataToJson(prettyPrint, responseParameters, transactionData);
}
@GET
@@ -0,0 +1,37 @@
package org.mifosng.platform.api.data;
import java.math.BigDecimal;
import org.joda.time.LocalDate;
/**
* Immutable data object representing a loan transaction.
*/
public class LoanTransactionNewData {
@SuppressWarnings("unused")
private Long id;
@SuppressWarnings("unused")
private EnumOptionData type;
@SuppressWarnings("unused")
private LocalDate date;
@SuppressWarnings("unused")
private CurrencyData currency;
@SuppressWarnings("unused")
private BigDecimal amount;
public LoanTransactionNewData(
final EnumOptionData transactionsType,
final CurrencyData currency,
final LocalDate date,
final BigDecimal amount) {
this.type = transactionsType;
this.currency = currency;
this.date = date;
this.amount = amount;
}
}
@@ -23,6 +23,7 @@ import org.mifosng.platform.api.data.LoanAccountData;
import org.mifosng.platform.api.data.LoanChargeData;
import org.mifosng.platform.api.data.LoanProductData;
import org.mifosng.platform.api.data.LoanTransactionData;
import org.mifosng.platform.api.data.LoanTransactionNewData;
import org.mifosng.platform.api.data.NoteData;
import org.mifosng.platform.api.data.OfficeData;
import org.mifosng.platform.api.data.OfficeTransactionData;
@@ -102,6 +103,8 @@ public interface ApiJsonSerializerService {
String serializeLoanAccountDataToJson(boolean prettyPrint, Set<String> responseParameters, LoanAccountData loanAccount);
String serializeLoanTransactionDataToJson(boolean prettyPrint, Set<String> responseParameters, LoanTransactionData transaction);
String serializeLoanTransactionDataToJson(boolean prettyPrint, Set<String> responseParameters, LoanTransactionNewData newTransactionData);
String serializeChargeDataToJson(boolean prettyPrint, Set<String> responseParameters, Collection<ChargeData> charges);
@@ -25,6 +25,7 @@ import org.mifosng.platform.api.data.LoanAccountData;
import org.mifosng.platform.api.data.LoanChargeData;
import org.mifosng.platform.api.data.LoanProductData;
import org.mifosng.platform.api.data.LoanTransactionData;
import org.mifosng.platform.api.data.LoanTransactionNewData;
import org.mifosng.platform.api.data.NoteData;
import org.mifosng.platform.api.data.OfficeData;
import org.mifosng.platform.api.data.OfficeTransactionData;
@@ -174,6 +175,9 @@ public class GoogleGsonApiJsonSerializerService implements ApiJsonSerializerServ
private static final Set<String> LOAN_TRANSACTION_DATA_PARAMETERS = new HashSet<String>(
Arrays.asList("id", "transactionType", "date", "principal",
"interest", "total", "totalWaived", "overpaid"));
private static final Set<String> LOAN_TRANSACTION_NEW_DATA_PARAMETERS = new HashSet<String>(
Arrays.asList("id", "type", "date", "currency", "amount"));
private static final Set<String> CHARGES_DATA_PARAMETERS = new HashSet<String>(
Arrays.asList("id", "name", "amount", "currency", "active",
@@ -552,6 +556,17 @@ public class GoogleGsonApiJsonSerializerService implements ApiJsonSerializerServ
responseParameters);
return helper.serializedJsonFrom(gsonDeserializer, transaction);
}
@Override
public String serializeLoanTransactionDataToJson(final boolean prettyPrint,
final Set<String> responseParameters,
final LoanTransactionNewData transaction) {
final Gson gsonDeserializer = helper
.createGsonBuilderWithParameterExclusionSerializationStrategy(
LOAN_TRANSACTION_NEW_DATA_PARAMETERS, prettyPrint,
responseParameters);
return helper.serializedJsonFrom(gsonDeserializer, transaction);
}
@Override
public String serializeChargeDataToJson(final boolean prettyPrint,
@@ -55,7 +55,6 @@ public class AdhikarLoanRepaymentScheduleTransactionProcessor extends AbstractLo
* 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,
@@ -64,37 +63,47 @@ public class AdhikarLoanRepaymentScheduleTransactionProcessor extends AbstractLo
final Money transactionAmountUnprocessed) {
// pay of overdue and current interest due given transaction date
MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency();
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money interestWaivedPortion = Money.zero(transactionAmountRemaining.getCurrency());
Money chargesPortion = Money.zero(transactionAmountRemaining.getCurrency());
LoanRepaymentScheduleInstallment currentInstallmentBasedOnTransactionDate = nearestInstallment(loanTransaction.getTransactionDate(), installments);
if (loanTransaction.isWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
final Money principalPortion = Money.zero(transactionAmountRemaining.getCurrency());
final Money interestPortion = Money.zero(transactionAmountRemaining.getCurrency());
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
} else {
for (LoanRepaymentScheduleInstallment installment : installments) {
if (
installment.isInterestDue(currency) &&
(installment.isOverdueOn(loanTransaction.getTransactionDate()) || installment.getInstallmentNumber().equals(currentInstallmentBasedOnTransactionDate.getInstallmentNumber()))
) {
Money interestPortion = installment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
Money principalPortion = Money.zero(currency);
Money interestWaivedPortion = Money.zero(currency);
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, Money.zero(transactionAmountRemaining.getCurrency()));
LoanRepaymentScheduleInstallment currentInstallmentBasedOnTransactionDate = nearestInstallment(loanTransaction.getTransactionDate(), installments);
for (LoanRepaymentScheduleInstallment installment : installments) {
if (
installment.isInterestDue(currency) &&
(installment.isOverdueOn(loanTransaction.getTransactionDate()) || installment.getInstallmentNumber().equals(currentInstallmentBasedOnTransactionDate.getInstallmentNumber()))
) {
Money interestPortion = installment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
Money principalPortion = Money.zero(currency);
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, Money.zero(transactionAmountRemaining.getCurrency()));
}
}
// 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, interestWaivedPortion, Money.zero(transactionAmountRemaining.getCurrency()));
}
}
}
// 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);
Money interestWaivedPortion = Money.zero(currency);
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, Money.zero(transactionAmountRemaining.getCurrency()));
}
}
return transactionAmountRemaining;
}
@@ -119,18 +128,26 @@ public class AdhikarLoanRepaymentScheduleTransactionProcessor extends AbstractLo
final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
Money principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money interestWaivedPortion = Money.zero(transactionAmountRemaining.getCurrency());
loanTransaction.updateComponents(principalPortion, interestPortion,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.isWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
} else {
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
return transactionAmountRemaining;
}
@@ -79,20 +79,24 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends
final Money transactionAmountUnprocessed) {
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money interestPortion = currentInstallment
.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining
.minus(interestPortion);
Money principalPortion = currentInstallment
.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining
.minus(principalPortion);
Money interestWaivedPortion = Money.zero(transactionAmountRemaining.getCurrency());
loanTransaction.updateComponents(principalPortion, interestPortion, 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.isWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
} else {
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
return transactionAmountRemaining;
}
@@ -52,7 +52,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
newState = stateOf(LoanStatus.APPROVED, allowedLoanStatuses);
}
break;
case LOAN_REPAYMENT:
case LOAN_REPAYMENT_OR_WAIVER:
if (this.anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE, LoanStatus.CLOSED, LoanStatus.OVERPAID)) {
newState = stateOf(LoanStatus.ACTIVE, allowedLoanStatuses);
} else {
@@ -65,31 +65,38 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
final List<LoanRepaymentScheduleInstallment> installments,
final LoanTransaction loanTransaction,
final LocalDate transactionDate, final Money paymentInAdvance) {
final MonetaryCurrency currency = paymentInAdvance.getCurrency();
Money transactionAmountRemaining = paymentInAdvance;
MonetaryCurrency currency = paymentInAdvance.getCurrency();
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.isWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
} else {
// placeholders for tracking the breakdown of components relevant to
// this transaction.
Money principalPortion = Money.zero(currency);
Money interestPortion = Money.zero(currency);
Money interestWaivedPortion = Money.zero(currency);
if (currentInstallment.isPrincipalNotCompleted(currency)) {
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
if (currentInstallment.isPrincipalCompleted(currency)) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(currentInstallment.getInterest(currency));
}
if (currentInstallment.isPrincipalNotCompleted(currency)) {
principalPortion = currentInstallment
.payPrincipalComponent(transactionAmountRemaining);
if (currentInstallment.isPrincipalCompleted(currency)) {
interestWaivedPortion = currentInstallment
.waiveInterestComponent(currentInstallment
.getInterest(currency));
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, Money.zero(transactionAmountRemaining.getCurrency()));
transactionAmountRemaining = transactionAmountRemaining
.minus(principalPortion);
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
return transactionAmountRemaining;
}
@@ -101,18 +108,27 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
Money principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
Money interestWaivedPortion = Money.zero(transactionAmountRemaining.getCurrency());
loanTransaction.updateComponents(principalPortion, interestPortion, 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.isWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
} else {
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
return transactionAmountRemaining;
}
@@ -582,65 +582,42 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
}
}
public void waive(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
/**
*
* @param amount The amount of interest to be waived.
* @param transactionDate The date on which the interest waiver is to be processed against.
* @param defaultLoanLifecycleStateMachine
*
* @return {@link LoanTransaction} details of executed transaction
*/
public LoanTransaction waiveInterest(
final BigDecimal amount,
final LocalDate transactionDate,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
final Money amountToWaive = Money.of(loanCurrency(), amount);
final LoanTransaction waiveInterestTransaction = LoanTransaction.waiver(this, amountToWaive, transactionDate);
loanTransaction.updateLoan(this);
this.loanTransactions.add(loanTransaction);
LocalDate loanTransactionDate = loanTransaction.getTransactionDate();
if (loanTransactionDate.isBefore(this.getDisbursementDate())) {
final String errorMessage = "The transaction date cannot be before the loan disbursement date: "
+ getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("waive",
"cannot.be.before.disbursement.date", errorMessage,
loanTransactionDate, this.getDisbursementDate());
}
if (loanTransactionDate.isAfter(new LocalDate())) {
final String errorMessage = "The transaction date cannot be in the future.";
throw new InvalidLoanStateTransitionException("waive",
"cannot.be.a.future.date", errorMessage, loanTransactionDate);
}
if (getTotalOutstanding().isGreaterThan(this.getInArrearsTolerance())) {
final String errorMessage = "Waiver is only allowed when the total outstanding amount left on loan ("
+ getTotalOutstanding()
+ ") is less than the in arrears tolerance setting of "
+ getInArrearsTolerance().getAmount();
throw new InvalidLoanStateTransitionException("waive",
"cannot.exceed.in.arrears.tolerance.setting", errorMessage,
getTotalOutstanding(), getInArrearsTolerance());
}
Money waived = Money.of(getCurrency(), loanTransaction.getAmount());
if (waived.isGreaterThan(this.getInArrearsTolerance())) {
final String errorMessage = "The amount being waived cannot exceed the in arrears tolerance setting of "
+ getInArrearsTolerance().getAmount();
throw new InvalidLoanStateTransitionException("waive",
"cannot.exceed.in.arrears.tolerance.setting", errorMessage,
waived, getInArrearsTolerance());
}
doPostLoanTransactionChecks(loanTransaction, loanLifecycleStateMachine);
handleRepaymentOrWaiverTransaction(waiveInterestTransaction, loanLifecycleStateMachine, null);
return waiveInterestTransaction;
}
public void updateTotalChargesDueAtDisbursement(){
public void updateTotalChargesDueAtDisbursement() {
this.totalChargesDueAtDisbursement = deriveSumTotalOfChargesDueAtDisbursement();
}
public void makeRepayment(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
handleRepaymentTransaction(loanTransaction, loanLifecycleStateMachine, false);
handleRepaymentOrWaiverTransaction(loanTransaction, loanLifecycleStateMachine, null);
}
private void handleRepaymentTransaction(
private void handleRepaymentOrWaiverTransaction(
final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final boolean adjusted) {
final LoanLifecycleStateMachine loanLifecycleStateMachine,
final LoanTransaction adjustedTransaction) {
LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT, LoanStatus.fromInt(this.loanStatus));
LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
loanTransaction.updateLoan(this);
@@ -649,33 +626,44 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.loanTransactions.add(loanTransaction);
if (loanTransaction.isNotRepayment()) {
final String errorMessage = "A transaction of type repayment was expected but not received.";
if (loanTransaction.isNotRepayment() && loanTransaction.isNotWaiver()) {
final String errorMessage = "A transaction of type repayment or waiver was expected but not received.";
throw new InvalidLoanTransactionTypeException("transaction",
"is.not.a.repayment.transaction", errorMessage);
"is.not.a.repayment.or.waiver.transaction", errorMessage);
}
LocalDate loanTransactionDate = loanTransaction.getTransactionDate();
if (loanTransactionDate.isBefore(this.getDisbursementDate())) {
final String errorMessage = "The transaction date cannot be before the loan disbursement date: "
+ getApprovedOnDate().toString();
throw new InvalidLoanStateTransitionException("repayment",
throw new InvalidLoanStateTransitionException("transaction",
"cannot.be.before.disbursement.date", errorMessage,
loanTransactionDate, this.getDisbursementDate());
}
if (loanTransactionDate.isAfter(new LocalDate())) {
final String errorMessage = "The transaction date cannot be in the future.";
throw new InvalidLoanStateTransitionException("repayment",
throw new InvalidLoanStateTransitionException("transaction",
"cannot.be.a.future.date", errorMessage, loanTransactionDate);
}
List<LoanTransaction> repaymentsOrWaivers = retreiveListOfRepaymentOrWaiverTransactionsPostDisbursement();
if (loanTransaction.isWaiver()) {
Money totalInterestOutstandingOnLoan = getTotalInterestOutstandingOnLoan();
if (adjustedTransaction != null) {
totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount());
}
if (loanTransaction.getAmount(loanCurrency()).isGreaterThan(totalInterestOutstandingOnLoan)) {
final String errorMessage = "The amount of interest to waive cannot be greater than total interest outstanding on loan.";
throw new InvalidLoanStateTransitionException("waive.interest",
"amount.exceeds.total.outstanding.interest", errorMessage, loanTransaction.getAmount(), totalInterestOutstandingOnLoan.getAmount());
}
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor(this.transactionProcessingStrategy);
if (isTransactionChronologicallyLatest && !adjusted) {
if (isTransactionChronologicallyLatest && adjustedTransaction == null) {
loanRepaymentScheduleTransactionProcessor.handleTransaction(loanTransaction, getCurrency(), this.repaymentScheduleInstallments);
} else {
final List<LoanTransaction> repaymentsOrWaivers = retreiveListOfRepaymentOrWaiverTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(repaymentsOrWaivers, getCurrency(), this.repaymentScheduleInstallments);
}
@@ -777,8 +765,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
public Money possibleNextRepaymentAmount() {
MonetaryCurrency currency = this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency();
MonetaryCurrency currency = this.loanRepaymentScheduleDetail.getPrincipal().getCurrency();
Money possibleNextRepaymentAmount = Money.zero(currency);
for (LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) {
@@ -790,26 +777,43 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
return possibleNextRepaymentAmount;
}
public LoanTransaction deriveDefaultInterestWaiverTransaction() {
final Money totalInterestOutstanding = getTotalInterestOutstandingOnLoan();
Money possibleInterestToWaive = totalInterestOutstanding.copy();
LocalDate transactionDate = new LocalDate();
if (totalInterestOutstanding.isGreaterThanZero()) {
// find earliest known instance of overdue interest and default to that
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
final Money outstandingForPeriod = scheduledRepayment.getInterestOutstanding(loanCurrency());
if (scheduledRepayment.isOverdueOn(new LocalDate()) && scheduledRepayment.isNotFullyCompleted() && outstandingForPeriod.isGreaterThanZero()) {
transactionDate = scheduledRepayment.getDueDate();
possibleInterestToWaive = outstandingForPeriod;
break;
}
}
}
return LoanTransaction.waiver(this, possibleInterestToWaive, transactionDate);
}
public void adjustExistingTransaction(
LoanTransaction transactionForAdjustment,
LoanTransaction newTransactionDetail,
LoanLifecycleStateMachine loanLifecycleStateMachine) {
final LoanTransaction transactionForAdjustment,
final LoanTransaction newTransactionDetail,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
if (transactionForAdjustment.isNotRepayment()
&& transactionForAdjustment.isNotWaiver()) {
if (transactionForAdjustment.isNotRepayment() && transactionForAdjustment.isNotWaiver()) {
final String errorMessage = "A transaction of type repayment or waiver was expected but not received.";
throw new InvalidLoanTransactionTypeException("transaction",
"is.not.a.repayment.or.waiver.transaction", errorMessage);
}
transactionForAdjustment.contra();
if (newTransactionDetail.isRepayment()) {
handleRepaymentTransaction(newTransactionDetail, loanLifecycleStateMachine, true);
}
if (newTransactionDetail.isWaiver()) {
waive(newTransactionDetail, loanLifecycleStateMachine);
if (newTransactionDetail.isRepayment() || newTransactionDetail.isWaiver()) {
handleRepaymentOrWaiverTransaction(newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment);
}
}
@@ -1240,8 +1244,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
private Money getTotalInterestOnLoan() {
Money cumulativeInterest = Money.zero(this.loanRepaymentScheduleDetail
.getPrincipal().getCurrency());
Money cumulativeInterest = Money.zero(loanCurrency());
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment
@@ -1250,6 +1253,31 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
return cumulativeInterest;
}
private Money getTotalInterestOutstandingOnLoan() {
Money cumulativeInterest = Money.zero(loanCurrency());
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
cumulativeInterest = cumulativeInterest.plus(scheduledRepayment.getInterestOutstanding(loanCurrency()));
}
return cumulativeInterest;
}
@SuppressWarnings("unused")
private Money getTotalInterestOverdueOnLoan() {
Money cumulativeInterestOverdue = Money.zero(this.loanRepaymentScheduleDetail.getPrincipal().getCurrency());
for (LoanRepaymentScheduleInstallment scheduledRepayment : this.repaymentScheduleInstallments) {
final Money interestOutstandingForPeriod = scheduledRepayment.getInterestOutstanding(loanCurrency());
if (scheduledRepayment.isOverdueOn(new LocalDate())) {
cumulativeInterestOverdue = cumulativeInterestOverdue.plus(interestOutstandingForPeriod);
}
}
return cumulativeInterestOverdue;
}
private Money getTotalPrincipalOnLoan() {
Money cumulativePrincipal = Money.zero(this.loanRepaymentScheduleDetail
@@ -2,7 +2,7 @@ package org.mifosng.platform.loan.domain;
public enum LoanEvent {
LOAN_CREATED, LOAN_REJECTED, LOAN_WITHDRAWN, LOAN_APPROVED, LOAN_APPROVAL_UNDO, LOAN_DISBURSED, LOAN_DISBURSAL_UNDO, LOAN_REPAYMENT,
LOAN_CREATED, LOAN_REJECTED, LOAN_WITHDRAWN, LOAN_APPROVED, LOAN_APPROVAL_UNDO, LOAN_DISBURSED, LOAN_DISBURSAL_UNDO, LOAN_REPAYMENT_OR_WAIVER,
//
REPAID_IN_FULL, LOAN_WRITE_OFF, LOAN_RESCHEDULE, INTERST_REBATE_OWED, LOAN_OVERPAYMENT;
}
@@ -93,6 +93,10 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
return Money.of(currency, this.principalCompleted);
}
public Money getPrincipalOutstanding(final MonetaryCurrency currency) {
return getPrincipal(currency).minus(getPrincipalCompleted(currency));
}
public Money getInterest(final MonetaryCurrency currency) {
return Money.of(currency, this.interest);
}
@@ -104,15 +108,23 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
public Money getInterestWaived(final MonetaryCurrency currency) {
return Money.of(currency, this.interestWaived);
}
public Money getInterestOutstanding(final MonetaryCurrency currency) {
return getInterest(currency).minus(getInterestCompleted(currency).plus(getInterestWaived(currency)));
}
public boolean isInterestDue(final MonetaryCurrency currency) {
return getInterest(currency).minus(getInterestCompleted(currency).plus(getInterestWaived(currency))).isGreaterThanZero();
}
public Money getTotal(MonetaryCurrency currency) {
public Money getTotal(final MonetaryCurrency currency) {
return getPrincipal(currency).plus(getInterest(currency));
}
public Money getTotalOutstanding(final MonetaryCurrency currency) {
return getPrincipalOutstanding(currency).plus(getInterestOutstanding(currency));
}
public void updateLoan(final Loan loan) {
this.loan = loan;
}
@@ -151,12 +163,10 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
public Money payInterestComponent(final Money transactionAmountRemaining) {
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money interestPortionOfTransaction = Money.zero(currency);
Money interestExpected = getInterest(currency);
Money interestDue = interestExpected.minus(getInterestCompleted(currency));
final Money interestDue = getInterestOutstanding(currency);
if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestDue)) {
this.interestCompleted = getInterestCompleted(currency).plus(interestDue).getAmount();
interestPortionOfTransaction = interestPortionOfTransaction.plus(interestDue);
@@ -172,11 +182,10 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
public Money payPrincipalComponent(final Money transactionAmountRemaining) {
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
final MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money principalPortionOfTransaction = Money.zero(currency);
Money principalExpected = getPrincipal(currency);
Money principalDue = principalExpected.minus(getPrincipalCompleted(currency));
final Money principalDue = getPrincipalOutstanding(currency);
if (transactionAmountRemaining.isGreaterThanOrEqualTo(principalDue)) {
this.principalCompleted = getPrincipalCompleted(currency).plus(principalDue).getAmount();
principalPortionOfTransaction = principalPortionOfTransaction.plus(principalDue);
@@ -194,8 +203,7 @@ public class LoanRepaymentScheduleInstallment extends AbstractAuditableCustom<Ap
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money waivedInterestPortionOfTransaction = Money.zero(currency);
Money interestExpected = getInterest(currency);
Money interestDue = interestExpected.minus(getInterestCompleted(currency));
Money interestDue = getInterestOutstanding(currency);
if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestDue)) {
this.interestWaived = getInterestWaived(currency).plus(interestDue).getAmount();
waivedInterestPortionOfTransaction = waivedInterestPortionOfTransaction.plus(interestDue);
@@ -69,23 +69,23 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
}
public static LoanTransaction disbursement(final Money amount, final LocalDate disbursementDate) {
return new LoanTransaction(LoanTransactionType.DISBURSEMENT, amount.getAmount(), disbursementDate);
return new LoanTransaction(null, LoanTransactionType.DISBURSEMENT, amount.getAmount(), disbursementDate);
}
public static LoanTransaction repayment(final Money amount, final LocalDate paymentDate) {
return new LoanTransaction(LoanTransactionType.REPAYMENT, amount.getAmount(), paymentDate);
return new LoanTransaction(null, LoanTransactionType.REPAYMENT, amount.getAmount(), paymentDate);
}
public static LoanTransaction repaymentAtDisbursement(final Money amount, final LocalDate paymentDate) {
return new LoanTransaction(LoanTransactionType.REPAYMENT_AT_DISBURSEMENT, amount.getAmount(), paymentDate);
return new LoanTransaction(null, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT, amount.getAmount(), paymentDate);
}
public static LoanTransaction waiver(final Money waived, final LocalDate waiveDate) {
return new LoanTransaction(LoanTransactionType.WAIVED, waived.getAmount(), waiveDate);
public static LoanTransaction waiver(final Loan loan, final Money waived, final LocalDate waiveDate) {
return new LoanTransaction(loan, LoanTransactionType.WAIVED, waived.getAmount(), waiveDate);
}
private static LoanTransaction contra(LoanTransaction originalTransaction) {
LoanTransaction contra = new LoanTransaction(LoanTransactionType.REVERSAL, originalTransaction.getAmount().negate(), new LocalDate(originalTransaction.getDateOf()));
private static LoanTransaction contra(final LoanTransaction originalTransaction) {
LoanTransaction contra = new LoanTransaction(null, LoanTransactionType.REVERSAL, originalTransaction.getAmount().negate(), new LocalDate(originalTransaction.getDateOf()));
contra.updateContra(originalTransaction);
return contra;
}
@@ -94,7 +94,8 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
this.contra = transaction;
}
private LoanTransaction(final LoanTransactionType type, final BigDecimal amount, final LocalDate date) {
private LoanTransaction(final Loan loan, final LoanTransactionType type, final BigDecimal amount, final LocalDate date) {
this.loan = loan;
this.typeOf = type.getValue();
this.amount = amount;
this.dateOf = date.toDateMidnight().toDate();
@@ -3,13 +3,28 @@ package org.mifosng.platform.loan.domain;
import java.util.Comparator;
/**
*
* Sort loan transactions by transaction date and transaction type placing
*/
public class LoanTransactionComparator implements Comparator<LoanTransaction> {
@Override
public int compare(final LoanTransaction o1, final LoanTransaction o2) {
return o1.getTransactionDate().compareTo(o2.getTransactionDate());
int compareResult = 0;
final int comparsion = o1.getTransactionDate().compareTo(o2.getTransactionDate());
if (comparsion == 0) {
// equal transaction dates
if (o1.isWaiver() && o2.isNotWaiver()) {
compareResult = -1;
} else if (o1.isNotWaiver() && o2.isWaiver()) {
compareResult = 1;
} else {
compareResult = 0;
}
} else {
compareResult = comparsion;
}
return compareResult;
}
}
@@ -68,22 +68,26 @@ public class MifosStyleLoanRepaymentScheduleTransactionProcessor extends
final LoanRepaymentScheduleInstallment currentInstallment,
final LoanTransaction loanTransaction,
final Money transactionAmountUnprocessed) {
Money transactionAmountRemaining = transactionAmountUnprocessed;
Money interestPortion = currentInstallment
.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining
.minus(interestPortion);
Money principalPortion = currentInstallment
.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining
.minus(principalPortion);
Money interestWaivedPortion = Money.zero(transactionAmountRemaining.getCurrency());
loanTransaction.updateComponents(principalPortion, interestPortion,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.isWaiver()) {
interestWaivedPortion = currentInstallment.waiveInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestWaivedPortion);
} else {
interestPortion = currentInstallment.payInterestComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion);
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, interestWaivedPortion, chargesPortion);
return transactionAmountRemaining;
}
@@ -10,6 +10,7 @@ import org.mifosng.platform.api.data.LoanBasicDetailsData;
import org.mifosng.platform.api.data.LoanPermissionData;
import org.mifosng.platform.api.data.LoanRepaymentTransactionData;
import org.mifosng.platform.api.data.LoanTransactionData;
import org.mifosng.platform.api.data.LoanTransactionNewData;
public interface LoanReadPlatformService {
@@ -29,7 +30,7 @@ public interface LoanReadPlatformService {
LoanTransactionData retrieveNewLoanRepaymentDetails(Long loanId);
LoanTransactionData retrieveNewLoanWaiverDetails(Long loanId);
LoanTransactionNewData retrieveNewLoanWaiveInterestDetails(Long loanId);
LoanTransactionData retrieveLoanTransactionDetails(Long loanId, Long transactionId);
}
@@ -20,6 +20,7 @@ import org.mifosng.platform.api.data.LoanProductData;
import org.mifosng.platform.api.data.LoanRepaymentTransactionData;
import org.mifosng.platform.api.data.LoanSchedulePeriodData;
import org.mifosng.platform.api.data.LoanTransactionData;
import org.mifosng.platform.api.data.LoanTransactionNewData;
import org.mifosng.platform.api.data.MoneyData;
import org.mifosng.platform.client.service.ClientReadPlatformService;
import org.mifosng.platform.currency.domain.ApplicationCurrency;
@@ -285,40 +286,30 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
}
@Override
public LoanTransactionData retrieveNewLoanWaiverDetails(final Long loanId) {
public LoanTransactionNewData retrieveNewLoanWaiveInterestDetails(final Long loanId) {
context.authenticatedUser();
// TODO - OPTIMIZE - write simple sql query to fetch back date of
// possible next transaction date.
// TODO - OPTIMIZE - write simple sql query to fetch back overdue interest that can be waived along with the date of repayment period interest is overdue.
Loan loan = this.loanRepository.findOne(loanId);
if (loan == null) {
throw new LoanNotFoundException(loanId);
}
final String currencyCode = loan.repaymentScheduleDetail()
.getPrincipal().getCurrencyCode();
ApplicationCurrency currency = this.applicationCurrencyRepository
.findOneByCode(currencyCode);
final String currencyCode = loan.repaymentScheduleDetail().getPrincipal().getCurrencyCode();
ApplicationCurrency currency = this.applicationCurrencyRepository.findOneByCode(currencyCode);
if (currency == null) {
throw new CurrencyNotFoundException(currencyCode);
}
CurrencyData currencyData = new CurrencyData(currency.getCode(),
currency.getName(), currency.getDecimalPlaces(),
currency.getDisplaySymbol(), currency.getNameCode());
Money totalOutstanding = loan.getTotalOutstanding();
MoneyData totalOutstandingData = MoneyData.of(currencyData,
totalOutstanding.getAmount());
LoanTransaction waiveOfInterest = loan.deriveDefaultInterestWaiverTransaction();
LoanTransactionData newWaiverDetails = new LoanTransactionData();
newWaiverDetails.setTransactionType(LoanEnumerations
.transactionType(LoanTransactionType.WAIVED));
newWaiverDetails.setDate(new LocalDate());
newWaiverDetails.setTotal(totalOutstandingData);
return newWaiverDetails;
EnumOptionData waiverEnum = LoanEnumerations.transactionType(LoanTransactionType.WAIVED);
return new LoanTransactionNewData(waiverEnum, currencyData, waiveOfInterest.getTransactionDate(), waiveOfInterest.getAmount());
}
@Override
@@ -45,7 +45,7 @@ public interface LoanWritePlatformService {
EntityIdentifier adjustLoanTransaction(AdjustLoanTransactionCommand command);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier waiveLoanAmount(LoanTransactionCommand command);
EntityIdentifier waiveInterestOnLoan(LoanTransactionCommand command);
@PreAuthorize(value = "hasRole('ORGANISATION_ADMINISTRATION_SUPER_USER_ROLE')")
EntityIdentifier addLoanCharge(LoanChargeCommand command);
@@ -495,7 +495,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
LocalDate transactionDate = command.getTransactionDate();
LoanTransaction newTransactionDetail = LoanTransaction.repayment(transactionAmount, transactionDate);
if (transactionToAdjust.isWaiver()) {
newTransactionDetail = LoanTransaction.waiver(transactionAmount, transactionDate);
newTransactionDetail = LoanTransaction.waiver(loan, transactionAmount, transactionDate);
}
loan.adjustExistingTransaction(transactionToAdjust, newTransactionDetail, defaultLoanLifecycleStateMachine());
@@ -515,33 +515,26 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional
@Override
public EntityIdentifier waiveLoanAmount(LoanTransactionCommand command) {
public EntityIdentifier waiveInterestOnLoan(final LoanTransactionCommand command) {
context.authenticatedUser();
LoanTransactionCommandValidator validator = new LoanTransactionCommandValidator(command);
final LoanTransactionCommandValidator validator = new LoanTransactionCommandValidator(command);
validator.validate();
Loan loan = this.loanRepository.findOne(command.getLoanId());
final Loan loan = this.loanRepository.findOne(command.getLoanId());
if (loan == null) {
throw new LoanNotFoundException(command.getLoanId());
}
Money waived = Money.of(loan.repaymentScheduleDetail()
.getPrincipal().getCurrency(),
command.getTransactionAmount());
LoanTransaction waiver = LoanTransaction.waiver(waived, command.getTransactionDate());
loan.waive(waiver, defaultLoanLifecycleStateMachine());
this.loanTransactionRepository.save(waiver);
final LoanTransaction waiveTransaction = loan.waiveInterest(command.getTransactionAmount(), command.getTransactionDate(), defaultLoanLifecycleStateMachine());
this.loanTransactionRepository.save(waiveTransaction);
this.loanRepository.save(loan);
String noteText = command.getNote();
final String noteText = command.getNote();
if (StringUtils.isNotBlank(noteText)) {
Note note = Note.loanTransactionNote(loan, waiver, noteText);
final Note note = Note.loanTransactionNote(loan, waiveTransaction, noteText);
this.noteRepository.save(note);
}