Ability to waive loan charge in full (integrated into repayment schedule) even if loan charge is marked as paid or due for payment in past or future.

This commit is contained in:
Keith Woodlock
2012-11-08 10:26:08 +00:00
parent b7a951b01a
commit 027be9df4f
23 changed files with 497 additions and 93 deletions
@@ -0,0 +1,6 @@
alter table m_loan_charge
add column `amount_waived_derived` decimal(19,6) DEFAULT NULL after `amount_paid_derived`;
alter table m_loan_charge
add column `amount_writtenoff_derived` decimal(19,6) DEFAULT NULL after `amount_waived_derived`;
@@ -65,6 +65,10 @@ public class LoanScheduleData {
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesPaid;
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesWaived;
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesWrittenOff;
@SuppressWarnings("unused")
private final BigDecimal cumulativeFeeChargesOutstanding;
@SuppressWarnings("unused")
@@ -72,6 +76,10 @@ public class LoanScheduleData {
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesPaid;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesWaived;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesWrittenOff;
@SuppressWarnings("unused")
private final BigDecimal cumulativePenaltyChargesOutstanding;
/**
@@ -144,10 +152,14 @@ public class LoanScheduleData {
this.cumulativeFeeChargesToDate = null;
this.cumulativeFeeChargesPaid = null;
this.cumulativeFeeChargesWaived = null;
this.cumulativeFeeChargesWrittenOff = null;
this.cumulativeFeeChargesOutstanding = null;
this.cumulativePenaltyChargesToDate = null;
this.cumulativePenaltyChargesPaid = null;
this.cumulativePenaltyChargesWaived = null;
this.cumulativePenaltyChargesWrittenOff = null;
this.cumulativePenaltyChargesOutstanding = null;
this.totalCostOfLoan = null;
@@ -192,10 +204,14 @@ public class LoanScheduleData {
this.cumulativeFeeChargesToDate = cumulativeFeeChargesToDate;
this.cumulativeFeeChargesPaid = null;
this.cumulativeFeeChargesWaived = null;
this.cumulativeFeeChargesWrittenOff = null;
this.cumulativeFeeChargesOutstanding = null;
this.cumulativePenaltyChargesToDate = null;
this.cumulativePenaltyChargesPaid = null;
this.cumulativePenaltyChargesWaived = null;
this.cumulativePenaltyChargesWrittenOff = null;
this.cumulativePenaltyChargesOutstanding = null;
if (cumulativeFeeChargesToDate != null) {
@@ -231,9 +247,13 @@ public class LoanScheduleData {
final BigDecimal cumulativeInterestOutstanding,
final BigDecimal cumulativeFeeChargesToDate,
final BigDecimal cumulativeFeeChargesPaid,
final BigDecimal cumulativeFeeChargesWaived,
final BigDecimal cumulativeFeeChargesWrittenOff,
final BigDecimal cumulativeFeeChargesOutstanding,
final BigDecimal cumulativePenaltyChargesToDate,
final BigDecimal cumulativePenaltyChargesPaid,
final BigDecimal cumulativePenaltyChargesWaived,
final BigDecimal cumulativePenaltyChargesWrittenOff,
final BigDecimal cumulativePenaltyChargesOutstanding,
final BigDecimal totalCostOfLoan,
final BigDecimal totalExpectedRepayment,
@@ -258,9 +278,13 @@ public class LoanScheduleData {
this.cumulativeInterestOutstanding = cumulativeInterestOutstanding;
this.cumulativeFeeChargesToDate = cumulativeFeeChargesToDate;
this.cumulativeFeeChargesPaid = cumulativeFeeChargesPaid;
this.cumulativeFeeChargesWaived = cumulativeFeeChargesWaived;
this.cumulativeFeeChargesWrittenOff = cumulativeFeeChargesWrittenOff;
this.cumulativeFeeChargesOutstanding = cumulativeFeeChargesOutstanding;
this.cumulativePenaltyChargesToDate = cumulativePenaltyChargesToDate;
this.cumulativePenaltyChargesPaid = cumulativePenaltyChargesPaid;
this.cumulativePenaltyChargesWaived = cumulativePenaltyChargesWaived;
this.cumulativePenaltyChargesWrittenOff = cumulativePenaltyChargesWrittenOff;
this.cumulativePenaltyChargesOutstanding = cumulativePenaltyChargesOutstanding;
this.totalCostOfLoan = totalCostOfLoan;
this.totalExpectedRepayment = totalExpectedRepayment;
@@ -518,6 +518,31 @@ public class LoansApiResource {
return Response.ok().entity(identifier).build();
}
@POST
@Path("{loanId}/charges/{chargeId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String waiveLoanCharge(
@PathParam("loanId") final Long loanId,
@PathParam("chargeId") final Long loanChargeId,
@QueryParam("command") final String commandParam,
final String jsonRequestBody){
// LoanChargeCommand command = this.apiDataConversionService.convertJsonToLoanChargeCommand(loanChargeId, loanId, jsonRequestBody);
LoanChargeCommand command = LoanChargeCommand.forWaiver(loanChargeId, loanId);
String json = "";
if (is(commandParam, "waive")) {
EntityIdentifier identifier = this.loanWritePlatformService.waiveLoanCharge(command);
json = this.apiJsonSerializerService.serializeEntityIdentifier(identifier);
} else {
throw new UnrecognizedQueryParamException("command", commandParam);
}
return json;
}
@DELETE
@Path("{loanId}/charges/{chargeId}")
@@ -1,6 +1,7 @@
package org.mifosng.platform.api.commands;
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.Set;
import org.joda.time.LocalDate;
@@ -23,7 +24,12 @@ public class LoanChargeCommand {
* Used to capture what parameters were passed in the json api request.
* It does not indicate that these values are modified from their original values when tyring to update.
*/
private final Set<String> parametersPassedInCommand;
private final Set<String> requestParameters;
public static LoanChargeCommand forWaiver(final Long id, final Long loanId) {
final Set<String> parametersPassedInCommand = new HashSet<String>();
return new LoanChargeCommand(parametersPassedInCommand, id, loanId, null, null, null, null, null);
}
public LoanChargeCommand(
final Set<String> parametersPassedInCommand,
@@ -34,7 +40,7 @@ public class LoanChargeCommand {
final Integer chargeTimeType,
final Integer chargeCalculationType,
final LocalDate specifiedDueDate) {
this.parametersPassedInCommand = parametersPassedInCommand;
this.requestParameters = parametersPassedInCommand;
this.id = id;
this.chargeId = chargeId;
this.loanId = loanId;
@@ -73,18 +79,18 @@ public class LoanChargeCommand {
}
public boolean isAmountChanged(){
return this.parametersPassedInCommand.contains("amount");
return this.requestParameters.contains("amount");
}
public boolean isChargeTimeTypeChanged(){
return this.parametersPassedInCommand.contains("chargeTimeType");
return this.requestParameters.contains("chargeTimeType");
}
public boolean isSpecifiedDueDateChanged(){
return this.parametersPassedInCommand.contains("specifiedDueDate");
return this.requestParameters.contains("specifiedDueDate");
}
public boolean isChargeCalculationTypeChanged(){
return this.parametersPassedInCommand.contains("chargeCalculationType");
return this.requestParameters.contains("chargeCalculationType");
}
}
@@ -41,6 +41,10 @@ public class LoanChargeData {
@SuppressWarnings("unused")
private final BigDecimal amountPaid;
@SuppressWarnings("unused")
private final BigDecimal amountWaived;
@SuppressWarnings("unused")
private final BigDecimal amountWrittenOff;
@SuppressWarnings("unused")
private final BigDecimal amountOutstanding;
@@ -79,6 +83,8 @@ public class LoanChargeData {
final CurrencyData currency,
final BigDecimal amount,
final BigDecimal amountPaid,
final BigDecimal amountWaived,
final BigDecimal amountWrittenOff,
final BigDecimal amountOutstanding,
final EnumOptionData chargeTimeType,
final LocalDate dueAsOfDate,
@@ -92,6 +98,8 @@ public class LoanChargeData {
this.currency = currency;
this.amount = amount;
this.amountPaid = amountPaid;
this.amountWaived = amountWaived;
this.amountWrittenOff = amountWrittenOff;
this.amountOutstanding = amountOutstanding;
this.chargeTimeType = chargeTimeType;
this.dueAsOfDate = dueAsOfDate;
@@ -109,7 +117,7 @@ public class LoanChargeData {
this.chargeOptions = null;
}
public LoanChargeData(
private LoanChargeData(
final Long id,
final Long chargeId,
final String name,
@@ -125,6 +133,8 @@ public class LoanChargeData {
this.currency = currency;
this.amount = amount;
this.amountPaid = BigDecimal.ZERO;
this.amountWaived = BigDecimal.ZERO;
this.amountWrittenOff = BigDecimal.ZERO;
this.amountOutstanding = amount;
this.chargeTimeType = chargeTimeType;
this.dueAsOfDate = null;
@@ -180,7 +180,11 @@ public class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService
public String loanChargeSchema(){
return "lc.id as id, c.id as chargeId, c.name as name, " +
"lc.amount as amountDue, lc.amount_paid_derived as amountPaid, lc.amount_outstanding_derived as amountOutstanding, " +
"lc.amount as amountDue, " +
"lc.amount_paid_derived as amountPaid, " +
"lc.amount_waived_derived as amountWaived, " +
"lc.amount_writtenoff_derived as amountWrittenOff, " +
"lc.amount_outstanding_derived as amountOutstanding, " +
"lc.calculation_percentage as percentageOf, lc.calculation_on_amount as amountPercentageAppliedTo, " +
"lc.charge_time_enum as chargeTime, " +
"lc.is_penalty as penalty, " +
@@ -201,6 +205,8 @@ public class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService
final String name = rs.getString("name");
final BigDecimal amount = rs.getBigDecimal("amountDue");
final BigDecimal amountPaid = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "amountPaid");
final BigDecimal amountWaived = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "amountWaived");
final BigDecimal amountWrittenOff = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "amountWrittenOff");
final BigDecimal amountOutstanding = rs.getBigDecimal("amountOutstanding");
final BigDecimal percentageOf = JdbcSupport.getBigDecimalDefaultToZeroIfNull(rs, "percentageOf");
@@ -224,7 +230,9 @@ public class ChargeReadPlatformServiceImpl implements ChargeReadPlatformService
final EnumOptionData chargeCalculationType = ChargeEnumerations.chargeCalculationType(chargeCalculation);
final boolean penalty = rs.getBoolean("penalty");
return new LoanChargeData(id, chargeId, name, currency, amount, amountPaid, amountOutstanding, chargeTimeType, dueAsOfDate, chargeCalculationType, percentageOf, amountPercentageAppliedTo, penalty);
return new LoanChargeData(id, chargeId, name, currency,
amount, amountPaid, amountWaived, amountWrittenOff,
amountOutstanding, chargeTimeType, dueAsOfDate, chargeCalculationType, percentageOf, amountPercentageAppliedTo, penalty);
}
}
}
@@ -24,6 +24,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
*/
@Override
public void handleTransaction(
final LocalDate disbursementDate,
final List<LoanTransaction> transactionsPostDisbursement,
final MonetaryCurrency currency,
final List<LoanRepaymentScheduleInstallment> installments,
@@ -31,7 +32,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
for (LoanCharge loanCharge : charges) {
if (!loanCharge.isDueAtDisbursement()) {
loanCharge.resetToOriginal();
loanCharge.resetPaidAmount(currency);
}
}
@@ -39,6 +40,10 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
currentInstallment.resetDerivedComponents();
}
// re-process loan charges over repayment periods (picking up on waived loan charges)
LoanScheduleWrapper wrapper = new LoanScheduleWrapper();
wrapper.reprocess(currency, disbursementDate, installments, charges);
for (LoanTransaction loanTransaction : transactionsPostDisbursement) {
if (loanTransaction.isRepayment() || loanTransaction.isInterestWaiver()) {
@@ -86,14 +91,16 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
installmentIndex++;
}
Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
if (feeCharges.isGreaterThanZero()) {
updateFeeChargesPaidAmountBy(feeCharges, charges);
}
Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
if (penaltyCharges.isGreaterThanZero()) {
updatePenaltyChargesPaidAmountBy(penaltyCharges, charges);
if (loanTransaction.isNotWaiver()) {
Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
if (feeCharges.isGreaterThanZero()) {
updateFeeChargesPaidAmountBy(feeCharges, charges);
}
Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
if (penaltyCharges.isGreaterThanZero()) {
updatePenaltyChargesPaidAmountBy(penaltyCharges, charges);
}
}
if (transactionAmountUnprocessed.isGreaterThanZero()) {
@@ -3,6 +3,7 @@ package org.mifosng.platform.loan.domain;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
/**
@@ -78,13 +79,21 @@ public class CreocoreLoanRepaymentScheduleTransactionProcessor extends
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.isInterestWaiver()) {
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 {
@@ -116,13 +116,21 @@ public class HeavensFamilyLoanRepaymentScheduleTransactionProcessor extends Abst
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.isInterestWaiver()) {
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 {
@@ -281,15 +281,15 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
updateTotalChargesDueAtDisbursement();
// reprocess loan schedule based on charge been removed.
LoanScheduleWrapper wrapper = new LoanScheduleWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), this.repaymentScheduleInstallments, this.charges);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor(this.transactionProcessingStrategy);
if (!loanCharge.isDueAtDisbursement()) { // TODO - only need to reprocess transactions against loan schedule if loan charge is added with due date before latest transaction.
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, this.charges);
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, this.charges);
} else {
// just reprocess the loan schedule only for now.
LoanScheduleWrapper wrapper = new LoanScheduleWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), this.repaymentScheduleInstallments, this.charges);
}
}
@@ -320,10 +320,6 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
updateTotalChargesDueAtDisbursement();
}
// reprocess loan schedule based on charge been removed.
LoanScheduleWrapper wrapper = new LoanScheduleWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), this.repaymentScheduleInstallments, this.charges);
// if removing disbursement charge - accomodate this on the transaction that tracked its payment
if (loanCharge.isDueAtDisbursement()) {
LoanTransaction transactionToRemove = null;
@@ -353,7 +349,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor(this.transactionProcessingStrategy);
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, this.charges);
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, this.charges);
}
}
@@ -364,6 +360,44 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
updateTotalChargesDueAtDisbursement();
}
}
public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
validateLoanIsNotClosed(loanCharge);
final Money amountWaived = loanCharge.waive(loanCurrency());
Money feeChargesWaived = Money.of(loanCurrency(), loanCharge.amount());
Money penaltyChargesWaived = Money.zero(loanCurrency());
if (loanCharge.isPenaltyCharge()) {
penaltyChargesWaived = Money.of(loanCurrency(), loanCharge.amount());
feeChargesWaived = Money.zero(loanCurrency());
}
LocalDate transactionDate = getDisbursementDate();
if (loanCharge.isSpecifiedDueDate()) {
transactionDate = loanCharge.getDueForCollectionAsOfLocalDate();
}
updateTotalChargesDueAtDisbursement();
final LoanTransaction waiveLoanChargeTransaction = LoanTransaction.waiveLoanCharge(this, amountWaived, transactionDate,feeChargesWaived, penaltyChargesWaived);
// reprocess loan schedule based on charge been added.
LoanScheduleWrapper wrapper = new LoanScheduleWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), this.repaymentScheduleInstallments, this.charges);
// Waive of charges whose due date falls after latest 'repayment' transaction dont require entire loan schedule to be reprocessed.
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor(this.transactionProcessingStrategy);
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, this.charges);
}
doPostLoanTransactionChecks(waiveLoanChargeTransaction.getTransactionDate(), loanLifecycleStateMachine);
return waiveLoanChargeTransaction;
}
public Client client() {
return this.client;
@@ -467,7 +501,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
final List<LoanTransaction> repaymentsOrWaivers = retreiveListOfTransactionsPostDisbursement();
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor(this.transactionProcessingStrategy);
loanRepaymentScheduleTransactionProcessor.handleTransaction(repaymentsOrWaivers, getCurrency(), this.repaymentScheduleInstallments, this.charges);
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), repaymentsOrWaivers, getCurrency(), this.repaymentScheduleInstallments, this.charges);
}
private boolean dateHasChanged(final LocalDate originalLocalDate, final LocalDate providedLocalDate) {
@@ -512,26 +546,19 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.termFrequency = loanTermFrequency;
this.termPeriodFrequencyType = loanTermFrequencyType.getValue();
this.submittedOnDate = submittedOn.toDateTimeAtCurrentTime().toDate();
// TODO - this should also match expectedDisbursmentDate + loan term
this.expectedMaturityDate = this.repaymentScheduleInstallments
.get(this.repaymentScheduleInstallments.size() - 1)
.getDueDate().toDateMidnight().toDate();
this.submittedOnDate = submittedOn.toDate();
this.expectedMaturityDate = determineExpectedMaturityDate().toDate();
if (expectedDisbursementDate != null) {
// can be null during bulk upload of loans
this.expectedDisbursedOnDate = expectedDisbursementDate
.toDateMidnight().toDate();
this.expectedDisbursedOnDate = expectedDisbursementDate.toDate();
}
if (repaymentsStartingFromDate != null) {
this.expectedFirstRepaymentOnDate = repaymentsStartingFromDate
.toDateMidnight().toDate();
this.expectedFirstRepaymentOnDate = repaymentsStartingFromDate.toDate();
}
if (interestChargedFromDate != null) {
this.interestChargedFromDate = interestChargedFromDate
.toDateMidnight().toDate();
this.interestChargedFromDate = interestChargedFromDate.toDate();
}
if (submittedOn.isAfter(new LocalDate())) {
@@ -553,6 +580,11 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
}
private LocalDate determineExpectedMaturityDate() {
int numberOfInstallments = this.repaymentScheduleInstallments.size();
return this.repaymentScheduleInstallments.get(numberOfInstallments - 1).getDueDate();
}
public void reject(final LocalDate rejectedOn, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, LoanStatus.fromInt(this.loanStatus));
@@ -718,7 +750,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.disbursedOnDate = null;
for (LoanCharge charge : this.charges) {
charge.resetToOriginal();
charge.resetToOriginal(loanCurrency());
}
for (LoanRepaymentScheduleInstallment currentInstallment : this.repaymentScheduleInstallments) {
@@ -770,7 +802,7 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.loanTransactions.add(loanTransaction);
}
if (loanTransaction.isNotRepayment() && loanTransaction.isNotInterestWaiver()) {
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.or.waiver.transaction", errorMessage);
@@ -808,10 +840,10 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
loanRepaymentScheduleTransactionProcessor.handleTransaction(loanTransaction, getCurrency(), this.repaymentScheduleInstallments, this.charges);
} else {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
loanRepaymentScheduleTransactionProcessor.handleTransaction(allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, this.charges);
loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), this.repaymentScheduleInstallments, this.charges);
}
doPostLoanTransactionChecks(loanTransaction, loanLifecycleStateMachine);
doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
}
private List<LoanTransaction> retreiveListOfTransactionsPostDisbursement() {
@@ -826,25 +858,27 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
return repaymentsOrWaivers;
}
private void doPostLoanTransactionChecks(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
private void doPostLoanTransactionChecks(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
if (this.isOverPaid()) {
handleLoanOverpayment(loanTransaction, loanLifecycleStateMachine);
handleLoanOverpayment(loanLifecycleStateMachine);
} else if (this.isRepaidInFull()) {
handleLoanRepaymentInFull(loanTransaction, loanLifecycleStateMachine);
// TODO - KW - probably should not close the loan automatically but let user decide if loan is closed or not and provide closing date.
// - need to dig into loan closure scenarios with MFIs
handleLoanRepaymentInFull(transactionDate, loanLifecycleStateMachine);
}
}
private void handleLoanRepaymentInFull(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
private void handleLoanRepaymentInFull(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
this.closedOnDate = loanTransaction.getTransactionDate().toDate();
this.maturedOnDate = loanTransaction.getTransactionDate().toDate();
this.closedOnDate = transactionDate.toDate();
this.maturedOnDate = transactionDate.toDate();
}
private void handleLoanOverpayment(@SuppressWarnings("unused") final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
private void handleLoanOverpayment(final LoanLifecycleStateMachine loanLifecycleStateMachine) {
LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
@@ -959,8 +993,8 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
final LoanTransaction newTransactionDetail,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
if (transactionForAdjustment.isNotRepayment() && transactionForAdjustment.isNotInterestWaiver()) {
final String errorMessage = "Only transactions of type repayment or interest waiver can be adjusted.";
if (transactionForAdjustment.isNotRepayment() && transactionForAdjustment.isNotWaiver()) {
final String errorMessage = "Only transactions of type repayment or waiver can be adjusted.";
throw new InvalidLoanTransactionTypeException("transaction",
"adjustment.is.only.allowed.to.repayment.or.waiver.transaction", errorMessage);
}
@@ -59,6 +59,12 @@ public class LoanCharge extends AbstractPersistable<Long> {
@Column(name = "amount_paid_derived", scale = 6, precision = 19, nullable = true)
private BigDecimal amountPaid;
@Column(name = "amount_waived_derived", scale = 6, precision = 19, nullable = true)
private BigDecimal amountWaived;
@Column(name = "amount_writtenoff_derived", scale = 6, precision = 19, nullable = true)
private BigDecimal amountWrittenOff;
@Column(name = "amount_outstanding_derived", scale = 6, precision = 19, nullable = false)
private BigDecimal amountOutstanding;
@@ -172,7 +178,9 @@ public class LoanCharge extends AbstractPersistable<Long> {
this.amount = null;
this.amountPercentageAppliedTo = null;
this.amountPaid = null;
this.amountOutstanding = null;
this.amountOutstanding = BigDecimal.ZERO;
this.amountWaived = null;
this.amountWrittenOff = null;
break;
case FLAT:
this.percentage = null;
@@ -180,6 +188,8 @@ public class LoanCharge extends AbstractPersistable<Long> {
this.amountPercentageAppliedTo = null;
this.amountPaid = null;
this.amountOutstanding = chargeAmount;
this.amountWaived = null;
this.amountWrittenOff = null;
break;
case PERCENT_OF_AMOUNT:
this.percentage = chargeAmount;
@@ -187,20 +197,26 @@ public class LoanCharge extends AbstractPersistable<Long> {
this.amount = percentageOf(this.amountPercentageAppliedTo, this.percentage);
this.amountPaid = null;
this.amountOutstanding = calculateOutstanding();
this.amountWaived = null;
this.amountWrittenOff = null;
break;
case PERCENT_OF_AMOUNT_AND_INTEREST:
this.percentage = null;
this.amount = null;
this.amountPercentageAppliedTo = null;
this.amountPaid = null;
this.amountOutstanding = null;
this.amountOutstanding = BigDecimal.ZERO;
this.amountWaived = null;
this.amountWrittenOff = null;
break;
case PERCENT_OF_INTEREST:
this.percentage = null;
this.amount = null;
this.amountPercentageAppliedTo = null;
this.amountPaid = null;
this.amountOutstanding = null;
this.amountOutstanding = BigDecimal.ZERO;
this.amountWaived = null;
this.amountWrittenOff = null;
break;
}
}
@@ -216,6 +232,36 @@ public class LoanCharge extends AbstractPersistable<Long> {
this.chargeTime = chargeTime;
this.chargeCalculation = chargeCalculation;
}
public void markAsFullyPaid() {
this.amountPaid = this.amount;
this.amountOutstanding = BigDecimal.ZERO;
this.paid = true;
}
public void resetToOriginal(final MonetaryCurrency currency) {
this.amountPaid = BigDecimal.ZERO;
this.amountWaived = BigDecimal.ZERO;
this.amountWrittenOff = BigDecimal.ZERO;
this.amountOutstanding = calculateAmountOutstanding(currency);
this.paid = false;
}
public void resetPaidAmount(final MonetaryCurrency currency) {
this.amountPaid = BigDecimal.ZERO;
this.amountOutstanding = calculateAmountOutstanding(currency);
this.paid = false;
}
public Money waive(final MonetaryCurrency currency) {
this.amountWaived = this.amount;
this.amountOutstanding = calculateAmountOutstanding(currency);
return getAmountWaived(currency);
}
private BigDecimal calculateAmountOutstanding(final MonetaryCurrency currency) {
return getAmount(currency).minus(getAmountWaived(currency)).minus(getAmountPaid(currency)).getAmount();
}
public void update(final Loan loan) {
this.loan = loan;
@@ -319,22 +365,10 @@ public class LoanCharge extends AbstractPersistable<Long> {
return percentageOf;
}
public void markAsFullyPaid() {
this.amountPaid = this.amount;
this.amountOutstanding = BigDecimal.ZERO;
this.paid = true;
}
public void resetToOriginal() {
this.amountPaid = BigDecimal.ZERO;
this.amountOutstanding = this.amount;
this.paid = false;
}
public BigDecimal amount() {
return this.amount;
}
public boolean hasNotLoanIdentifiedBy(final Long loanId) {
return !hasLoanIdentifiedBy(loanId);
}
@@ -373,7 +407,25 @@ public class LoanCharge extends AbstractPersistable<Long> {
}
public boolean isPaidOrPartiallyPaid(final MonetaryCurrency currency) {
return Money.of(currency, this.amountPaid).isGreaterThanZero();
final Money amountWaivedOrWrittenOff = getAmountWaived(currency).plus(getAmountWrittenOff(currency));
return Money.of(currency, this.amountPaid).plus(amountWaivedOrWrittenOff).isGreaterThanZero();
}
private Money getAmount(final MonetaryCurrency currency) {
return Money.of(currency, this.amount);
}
private Money getAmountPaid(final MonetaryCurrency currency) {
return Money.of(currency, this.amountPaid);
}
public Money getAmountWaived(final MonetaryCurrency currency) {
return Money.of(currency, this.amountWaived);
}
public Money getAmountWrittenOff(final MonetaryCurrency currency) {
return Money.of(currency, this.amountWrittenOff);
}
public Money updatePaidAmountBy(final Money incrementBy) {
@@ -372,6 +372,46 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
return waivedInterestPortionOfTransaction;
}
public Money waivePenaltyChargesComponent(final Money transactionAmountRemaining) {
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money waivedPenaltyChargesPortionOfTransaction = Money.zero(currency);
Money penanltiesDue = getPenaltyChargesOutstanding(currency);
if (transactionAmountRemaining.isGreaterThanOrEqualTo(penanltiesDue)) {
this.penaltyChargesWaived = getPenaltyChargesWaived(currency).plus(penanltiesDue).getAmount();
waivedPenaltyChargesPortionOfTransaction = waivedPenaltyChargesPortionOfTransaction.plus(penanltiesDue);
} else {
this.penaltyChargesWaived = getPenaltyChargesWaived(currency).plus(transactionAmountRemaining).getAmount();
waivedPenaltyChargesPortionOfTransaction = waivedPenaltyChargesPortionOfTransaction.plus(transactionAmountRemaining);
}
this.penaltyChargesWaived = defaultToNullIfZero(this.penaltyChargesWaived);
this.completed = getTotalOutstanding(currency).isZero();
return waivedPenaltyChargesPortionOfTransaction;
}
public Money waiveFeeChargesComponent(final Money transactionAmountRemaining) {
MonetaryCurrency currency = transactionAmountRemaining.getCurrency();
Money waivedFeeChargesPortionOfTransaction = Money.zero(currency);
Money feesDue = getPenaltyChargesOutstanding(currency);
if (transactionAmountRemaining.isGreaterThanOrEqualTo(feesDue)) {
this.feeChargesWaived = getFeeChargesWaived(currency).plus(feesDue).getAmount();
waivedFeeChargesPortionOfTransaction = waivedFeeChargesPortionOfTransaction.plus(feesDue);
} else {
this.feeChargesWaived = getFeeChargesWaived(currency).plus(transactionAmountRemaining).getAmount();
waivedFeeChargesPortionOfTransaction = waivedFeeChargesPortionOfTransaction.plus(transactionAmountRemaining);
}
this.feeChargesWaived = defaultToNullIfZero(this.feeChargesWaived);
this.completed = getTotalOutstanding(currency).isZero();
return waivedFeeChargesPortionOfTransaction;
}
public Money writeOffOutstandingPrincipal(final MonetaryCurrency currency) {
final Money principalDue = getPrincipalOutstanding(currency);
@@ -394,8 +434,18 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus
return this.getDueDate().isBefore(transactionDate);
}
public void updateChargePortion(final Money feeChargesDue, final Money penaltyChargesDue) {
public void updateChargePortion(
final Money feeChargesDue,
final Money feeChargesWaived,
final Money feeChargesWrittenOff,
final Money penaltyChargesDue,
final Money penaltyChargesWaived,
final Money penaltyChargesWrittenOff) {
this.feeCharges = defaultToNullIfZero(feeChargesDue.getAmount());
this.feeChargesWaived = defaultToNullIfZero(feeChargesWaived.getAmount());
this.feeChargesWrittenOff = defaultToNullIfZero(feeChargesWrittenOff.getAmount());
this.penaltyCharges = defaultToNullIfZero(penaltyChargesDue.getAmount());
this.penaltyChargesWaived = defaultToNullIfZero(penaltyChargesWaived.getAmount());
this.penaltyChargesWrittenOff = defaultToNullIfZero(penaltyChargesWrittenOff.getAmount());
}
}
@@ -3,13 +3,14 @@ package org.mifosng.platform.loan.domain;
import java.util.List;
import java.util.Set;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
public interface LoanRepaymentScheduleTransactionProcessor {
void handleTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments, Set<LoanCharge> charges);
void handleTransaction(List<LoanTransaction> repaymentsOrWaivers, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, Set<LoanCharge> charges);
void handleTransaction(LocalDate disbursementDate, List<LoanTransaction> repaymentsOrWaivers, MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments, Set<LoanCharge> charges);
void handleWriteOff(LoanTransaction loanTransaction,
MonetaryCurrency loanCurrency,
@@ -39,10 +39,16 @@ public class LoanScheduleWrapper {
LocalDate startDate = disbursementDate;
for (LoanRepaymentScheduleInstallment period : repaymentPeriods) {
// FIXME - kw - also need to handle case where some of these charges are paid/writtenoff/waived so that those components are updated on repayment period.
final Money feeChargesDueForRepaymentPeriod = cumulativeFeeChargesDueWithin(startDate, period.getDueDate(), loanCharges, currency);
final Money feeChargesWaivedForRepaymentPeriod = cumulativeFeeChargesWaivedWithin(startDate, period.getDueDate(), loanCharges, currency);
final Money feeChargesWrittenOffForRepaymentPeriod = cumulativeFeeChargesWrittenOffWithin(startDate, period.getDueDate(), loanCharges, currency);
final Money penaltyChargesDueForRepaymentPeriod = cumulativePenaltyChargesDueWithin(startDate, period.getDueDate(), loanCharges, currency);
period.updateChargePortion(feeChargesDueForRepaymentPeriod, penaltyChargesDueForRepaymentPeriod);
final Money penaltyChargesWaivedForRepaymentPeriod = cumulativePenaltyChargesWaivedWithin(startDate, period.getDueDate(), loanCharges, currency);
final Money penaltyChargesWrittenOffForRepaymentPeriod = cumulativePenaltyChargesWrittenOffWithin(startDate, period.getDueDate(), loanCharges, currency);
period.updateChargePortion(feeChargesDueForRepaymentPeriod, feeChargesWaivedForRepaymentPeriod, feeChargesWrittenOffForRepaymentPeriod,
penaltyChargesDueForRepaymentPeriod, penaltyChargesWaivedForRepaymentPeriod, penaltyChargesWrittenOffForRepaymentPeriod);
startDate = period.getDueDate();
}
@@ -65,13 +71,47 @@ public class LoanScheduleWrapper {
return cumulative;
}
private Money cumulativeFeeChargesWaivedWithin(
final LocalDate periodStart,
final LocalDate periodEnd,
final Set<LoanCharge> loanCharges,
final MonetaryCurrency currency) {
Money cumulative = Money.zero(currency);
for (LoanCharge loanCharge : loanCharges) {
if (loanCharge.isDueForCollectionBetween(periodStart, periodEnd) && loanCharge.isFeeCharge()) {
cumulative = cumulative.plus(loanCharge.getAmountWaived(currency));
}
}
return cumulative;
}
private Money cumulativeFeeChargesWrittenOffWithin(
final LocalDate periodStart,
final LocalDate periodEnd,
final Set<LoanCharge> loanCharges,
final MonetaryCurrency currency) {
Money cumulative = Money.zero(currency);
for (LoanCharge loanCharge : loanCharges) {
if (loanCharge.isDueForCollectionBetween(periodStart, periodEnd) && loanCharge.isFeeCharge()) {
cumulative = cumulative.plus(loanCharge.getAmountWrittenOff(currency));
}
}
return cumulative;
}
private Money cumulativePenaltyChargesDueWithin(
final LocalDate periodStart,
final LocalDate periodEnd,
final Set<LoanCharge> loanCharges,
final MonetaryCurrency monetaryCurrency) {
final MonetaryCurrency currency) {
Money cumulative = Money.zero(monetaryCurrency);
Money cumulative = Money.zero(currency);
for (LoanCharge loanCharge : loanCharges) {
if (loanCharge.isDueForCollectionBetween(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) {
@@ -81,4 +121,38 @@ public class LoanScheduleWrapper {
return cumulative;
}
private Money cumulativePenaltyChargesWaivedWithin(
final LocalDate periodStart,
final LocalDate periodEnd,
final Set<LoanCharge> loanCharges,
final MonetaryCurrency currency) {
Money cumulative = Money.zero(currency);
for (LoanCharge loanCharge : loanCharges) {
if (loanCharge.isDueForCollectionBetween(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) {
cumulative = cumulative.plus(loanCharge.getAmountWaived(currency));
}
}
return cumulative;
}
private Money cumulativePenaltyChargesWrittenOffWithin(
final LocalDate periodStart,
final LocalDate periodEnd,
final Set<LoanCharge> loanCharges,
final MonetaryCurrency currency) {
Money cumulative = Money.zero(currency);
for (LoanCharge loanCharge : loanCharges) {
if (loanCharge.isDueForCollectionBetween(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) {
cumulative = cumulative.plus(loanCharge.getAmountWrittenOff(currency));
}
}
return cumulative;
}
}
@@ -87,6 +87,13 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
return new LoanTransaction(loan, LoanTransactionType.WAIVE_INTEREST, waived.getAmount(), waiveDate);
}
public static LoanTransaction waiveLoanCharge(final Loan loan, final Money waived, final LocalDate waiveDate, final Money feeChargesWaived, final Money penaltyChargesWaived) {
LoanTransaction waiver = new LoanTransaction(loan, LoanTransactionType.WAIVE_CHARGES, waived.getAmount(), waiveDate);
waiver.updateChargesComponents(feeChargesWaived, penaltyChargesWaived);
return waiver;
}
private static LoanTransaction contra(final LoanTransaction originalTransaction) {
LoanTransaction contra = new LoanTransaction(null, LoanTransactionType.CONTRA, originalTransaction.getAmount().negate(), new LocalDate(originalTransaction.getDateOf()));
contra.updateContra(originalTransaction);
@@ -153,10 +160,22 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
return LoanTransactionType.WAIVE_INTEREST.equals(getTypeOf()) && isNotContra();
}
public boolean isChargesWaiver() {
return LoanTransactionType.WAIVE_CHARGES.equals(getTypeOf()) && isNotContra();
}
public boolean isNotInterestWaiver() {
return !isInterestWaiver();
}
public boolean isWaiver() {
return isInterestWaiver() || isChargesWaiver();
}
public boolean isNotWaiver() {
return !isInterestWaiver() && !isChargesWaiver();
}
public boolean isWriteOff() {
return getTypeOf().isWriteOff() && isNotContra();
}
@@ -195,6 +214,11 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
final MonetaryCurrency currency = principal.getCurrency();
this.principalPortion = defaultToNullIfZero(getPrincipalPortion(currency).plus(principal).getAmount());
this.interestPortion = defaultToNullIfZero(getInterestPortion(currency).plus(interest).getAmount());
updateChargesComponents(feeCharges, penaltyCharges);
}
private void updateChargesComponents(final Money feeCharges, final Money penaltyCharges) {
final MonetaryCurrency currency = feeCharges.getCurrency();
this.feeChargesPortion = defaultToNullIfZero(getFeeChargesPortion(currency).plus(feeCharges).getAmount());
this.penaltyChargesPortion = defaultToNullIfZero(getPenaltyChargesPortion(currency).plus(penaltyCharges).getAmount());
}
@@ -205,7 +229,7 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
final MonetaryCurrency currency = principal.getCurrency();
this.amount = getPrincipalPortion(currency).plus(getInterestPortion(currency)).plus(getFeeChargesPortion(currency)).plus(getPenaltyChargesPortion(currency)).getAmount();
}
public Money getPrincipalPortion(final MonetaryCurrency currency) {
return Money.of(currency, this.principalPortion);
}
@@ -226,6 +250,7 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
this.principalPortion = null;
this.interestPortion = null;
this.feeChargesPortion = null;
this.penaltyChargesPortion = null;
}
public boolean isGreaterThan(final Money monetaryAmount) {
@@ -13,9 +13,9 @@ public class LoanTransactionComparator implements Comparator<LoanTransaction> {
final int comparsion = o1.getTransactionDate().compareTo(o2.getTransactionDate());
if (comparsion == 0) {
// equal transaction dates
if (o1.isInterestWaiver() && o2.isNotInterestWaiver()) {
if (o1.isWaiver() && o2.isNotWaiver()) {
compareResult = -1;
} else if (o1.isNotInterestWaiver() && o2.isInterestWaiver()) {
} else if (o1.isNotWaiver() && o2.isWaiver()) {
compareResult = 1;
} else {
compareResult = 0;
@@ -13,7 +13,8 @@ public enum LoanTransactionType {
/**
* This type of transactions is allowed on written-off loans where mfi still attempts to recover payments from applicant after writing-off.
*/
RECOVERY_REPAYMENT(8, "loanTransactionType.recoveryRepayment");
RECOVERY_REPAYMENT(8, "loanTransactionType.recoveryRepayment"),
WAIVE_CHARGES(9, "loanTransactionType.waiveCharges");
private final Integer value;
private final String code;
@@ -60,6 +61,12 @@ public enum LoanTransactionType {
case 7:
loanTransactionType = LoanTransactionType.MARKED_FOR_RESCHEDULING;
break;
case 8:
loanTransactionType = LoanTransactionType.RECOVERY_REPAYMENT;
break;
case 9:
loanTransactionType = LoanTransactionType.WAIVE_CHARGES;
break;
default:
loanTransactionType = LoanTransactionType.INVALID;
break;
@@ -3,6 +3,7 @@ package org.mifosng.platform.loan.domain;
import java.util.List;
import org.joda.time.LocalDate;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
/**
@@ -69,15 +70,27 @@ public class MifosStyleLoanRepaymentScheduleTransactionProcessor extends
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.isInterestWaiver()) {
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);
@@ -90,9 +103,10 @@ public class MifosStyleLoanRepaymentScheduleTransactionProcessor extends
principalPortion = currentInstallment.payPrincipalComponent(transactionAmountRemaining);
transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion);
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
}
loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion);
return transactionAmountRemaining;
}
@@ -135,13 +135,22 @@ public class RBILoanRepaymentScheduleTransactionProcessor extends AbstractLoanRe
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.isInterestWaiver()) {
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 {
@@ -167,8 +167,8 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
return new LoanScheduleData(currency, periods, loanTermInDays,
cumulativePrincipalDisbursed, cumulativePrincipalDue, cumulativePrincipalPaid, cumulativePrincipalWrittenOff, cumulativePrincipalOutstanding,
cumulativeInterestExpected, cumulativeInterestPaid, cumulativeInterestWaived, cumulativeInterestWrittenOff, cumulativeInterestOutstanding,
cumulativeFeeChargesExpected, cumulativeFeeChargesPaid, cumulativeFeeChargesOutstanding,
cumulativePenaltyChargesExpected, cumulativePenaltyChargesPaid, cumulativePenaltyChargesOutstanding,
cumulativeFeeChargesExpected, cumulativeFeeChargesPaid, cumulativeFeeChargesWaived, cumulativeFeeChargesWrittenOff, cumulativeFeeChargesOutstanding,
cumulativePenaltyChargesExpected, cumulativePenaltyChargesPaid, cumulativePenaltyChargesWaived, cumulativePenaltyChargesWrittenOff, cumulativePenaltyChargesOutstanding,
totalExpectedCostOfLoan, totalExpectedRepayment, totalPaidToDate, totalWaivedToDate, totalWrittenOffToDate, totalOutstanding, totalInArrears);
} catch (EmptyResultDataAccessException e) {
throw new LoanNotFoundException(loanId);
@@ -581,7 +581,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService {
final BigDecimal totalDueForPeriod = principalDue.add(totalExpectedCostOfLoanForPeriod);
final BigDecimal totalPaidForPeriod = principalPaid.add(interestPaid).add(feeChargesPaid).add(penaltyChargesPaid);
final BigDecimal totalWaivedForPeriod = interestWaived.add(feeChargesWaived);
final BigDecimal totalWaivedForPeriod = interestWaived.add(feeChargesWaived).add(penaltyChargesWaived);
final BigDecimal totalWrittenOffForPeriod = principalWrittenOff.add(interestWrittenOff).add(feeChargesWrittenOff).add(penaltyChargesWrittenOff);
final BigDecimal totalOutstandingForPeriod = principalOutstanding.add(interestOutstanding).add(feeChargesOutstanding).add(penaltyChargesOutstanding);
@@ -57,15 +57,18 @@ public interface LoanWritePlatformService {
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier closeAsRescheduled(LoanTransactionCommand command);
@PreAuthorize(value = "hasRole('ORGANISATION_ADMINISTRATION_SUPER_USER_ROLE')")
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier addLoanCharge(LoanChargeCommand command);
@PreAuthorize(value = "hasRole('ORGANISATION_ADMINISTRATION_SUPER_USER_ROLE')")
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier updateLoanCharge(LoanChargeCommand command);
@PreAuthorize(value = "hasRole('ORGANISATION_ADMINISTRATION_SUPER_USER_ROLE')")
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier deleteLoanCharge(final Long loanId, final Long loanChargeId);
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier waiveLoanCharge(LoanChargeCommand command);
@PreAuthorize(value = "hasRole('ORGANISATION_ADMINISTRATION_SUPER_USER_ROLE')")
@PreAuthorize(value = "hasAnyRole('PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE')")
EntityIdentifier bulkLoanReassignment(final BulkLoanReassignmentCommand command);
}
@@ -666,6 +666,35 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
return new EntityIdentifier(loanCharge.getId());
}
@Transactional
@Override
public EntityIdentifier waiveLoanCharge(final LoanChargeCommand command) {
this.context.authenticatedUser();
// LoanChargeCommandValidator validator = new LoanChargeCommandValidator(command);
// validator.validateForUpdate();
final Long loanId = command.getLoanId();
final Loan loan = retrieveLoanBy(loanId);
final Long loanChargeId = command.getId();
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
final LoanTransaction waiveTransaction = loan.waiveLoanCharge(loanCharge, defaultLoanLifecycleStateMachine());
this.loanTransactionRepository.save(waiveTransaction);
this.loanRepository.save(loan);
final String noteText = ""; //command.getNote();
if (StringUtils.isNotBlank(noteText)) {
final Note note = Note.loanTransactionNote(loan, waiveTransaction, noteText);
this.noteRepository.save(note);
}
return new EntityIdentifier(loanCharge.getId());
}
@Transactional
@Override
@@ -200,6 +200,9 @@ public class LoanEnumerations {
case RECOVERY_REPAYMENT:
optionData = new EnumOptionData(LoanTransactionType.RECOVERY_REPAYMENT.getValue().longValue(), LoanTransactionType.RECOVERY_REPAYMENT.getCode(), "Repayment (after write-off)");
break;
case WAIVE_CHARGES:
optionData = new EnumOptionData(LoanTransactionType.WAIVE_CHARGES.getValue().longValue(), LoanTransactionType.WAIVE_CHARGES.getCode(), "Waive loan charges");
break;
}
return optionData;
}