From 027be9df4f100b78d8eab755061e4d7b37efb355 Mon Sep 17 00:00:00 2001 From: Keith Woodlock Date: Thu, 8 Nov 2012 10:26:08 +0000 Subject: [PATCH] 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. --- ...loancharges-waive-and-writeoff-support.sql | 6 + .../platform/api/LoanScheduleData.java | 24 ++++ .../platform/api/LoansApiResource.java | 25 +++++ .../api/commands/LoanChargeCommand.java | 18 ++- .../platform/api/data/LoanChargeData.java | 12 +- .../ChargeReadPlatformServiceImpl.java | 12 +- ...RepaymentScheduleTransactionProcessor.java | 25 +++-- ...RepaymentScheduleTransactionProcessor.java | 11 +- ...RepaymentScheduleTransactionProcessor.java | 10 +- .../mifosng/platform/loan/domain/Loan.java | 106 ++++++++++++------ .../platform/loan/domain/LoanCharge.java | 86 +++++++++++--- .../LoanRepaymentScheduleInstallment.java | 52 ++++++++- ...RepaymentScheduleTransactionProcessor.java | 3 +- .../loan/domain/LoanScheduleWrapper.java | 82 +++++++++++++- .../platform/loan/domain/LoanTransaction.java | 27 ++++- .../domain/LoanTransactionComparator.java | 4 +- .../loan/domain/LoanTransactionType.java | 9 +- ...RepaymentScheduleTransactionProcessor.java | 18 ++- ...RepaymentScheduleTransactionProcessor.java | 11 +- .../service/LoanReadPlatformServiceImpl.java | 6 +- .../service/LoanWritePlatformService.java | 11 +- ...WritePlatformServiceJpaRepositoryImpl.java | 29 +++++ .../loanproduct/service/LoanEnumerations.java | 3 + 23 files changed, 497 insertions(+), 93 deletions(-) create mode 100644 mifosng-db/patches/patch-0011-loancharges-waive-and-writeoff-support.sql diff --git a/mifosng-db/patches/patch-0011-loancharges-waive-and-writeoff-support.sql b/mifosng-db/patches/patch-0011-loancharges-waive-and-writeoff-support.sql new file mode 100644 index 000000000..f2231d644 --- /dev/null +++ b/mifosng-db/patches/patch-0011-loancharges-waive-and-writeoff-support.sql @@ -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`; + diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/LoanScheduleData.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/LoanScheduleData.java index ac8648685..4091b042f 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/LoanScheduleData.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/LoanScheduleData.java @@ -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; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java index f43d7af2f..4c50e584f 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/LoansApiResource.java @@ -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}") diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/LoanChargeCommand.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/LoanChargeCommand.java index be9b86a74..c0c98f26b 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/LoanChargeCommand.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/commands/LoanChargeCommand.java @@ -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 parametersPassedInCommand; + private final Set requestParameters; + + public static LoanChargeCommand forWaiver(final Long id, final Long loanId) { + final Set parametersPassedInCommand = new HashSet(); + return new LoanChargeCommand(parametersPassedInCommand, id, loanId, null, null, null, null, null); + } public LoanChargeCommand( final Set 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"); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanChargeData.java b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanChargeData.java index ea558b513..275cb8ac7 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanChargeData.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/api/data/LoanChargeData.java @@ -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; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/charge/service/ChargeReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/charge/service/ChargeReadPlatformServiceImpl.java index c9cafbb3e..65a5d6fcf 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/charge/service/ChargeReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/charge/service/ChargeReadPlatformServiceImpl.java @@ -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); } } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java index a51e05da0..010071788 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -24,6 +24,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen */ @Override public void handleTransaction( + final LocalDate disbursementDate, final List transactionsPostDisbursement, final MonetaryCurrency currency, final List 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()) { diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/CreocoreLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/CreocoreLoanRepaymentScheduleTransactionProcessor.java index 7cbfbe329..b27082c4a 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/CreocoreLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/CreocoreLoanRepaymentScheduleTransactionProcessor.java @@ -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 { diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java index 8d753fd18..d355c433a 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/HeavensFamilyLoanRepaymentScheduleTransactionProcessor.java @@ -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 { diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java index b5c4d5b66..1cb5f9d94 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java @@ -281,15 +281,15 @@ public class Loan extends AbstractAuditableCustom { 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 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 { 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 { final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor(this.transactionProcessingStrategy); if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) { final List 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 { 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 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 { final List 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 { 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 { } } + 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 { 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 { 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 { loanRepaymentScheduleTransactionProcessor.handleTransaction(loanTransaction, getCurrency(), this.repaymentScheduleInstallments, this.charges); } else { final List 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 retreiveListOfTransactionsPostDisbursement() { @@ -826,25 +858,27 @@ public class Loan extends AbstractAuditableCustom { 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 { 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); } diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanCharge.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanCharge.java index c4ccc1cc0..435e52206 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanCharge.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanCharge.java @@ -59,6 +59,12 @@ public class LoanCharge extends AbstractPersistable { @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 { 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 { 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 { 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 { 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 { 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 { } 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) { diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallment.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallment.java index 64bc79491..48ae1cf06 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallment.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleInstallment.java @@ -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()); } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleTransactionProcessor.java index 99c928831..d4ca3c7f6 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanRepaymentScheduleTransactionProcessor.java @@ -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 installments, Set charges); - void handleTransaction(List repaymentsOrWaivers, MonetaryCurrency currency, List repaymentScheduleInstallments, Set charges); + void handleTransaction(LocalDate disbursementDate, List repaymentsOrWaivers, MonetaryCurrency currency, List repaymentScheduleInstallments, Set charges); void handleWriteOff(LoanTransaction loanTransaction, MonetaryCurrency loanCurrency, diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanScheduleWrapper.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanScheduleWrapper.java index 40082a57c..65290b1dd 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanScheduleWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanScheduleWrapper.java @@ -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 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 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 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 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 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; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java index 9969c6f3d..e824be207 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java @@ -87,6 +87,13 @@ public class LoanTransaction extends AbstractAuditableCustom { 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 { 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 { 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 { 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 { this.principalPortion = null; this.interestPortion = null; this.feeChargesPortion = null; + this.penaltyChargesPortion = null; } public boolean isGreaterThan(final Money monetaryAmount) { diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionComparator.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionComparator.java index 68d1a58af..c90e157d2 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionComparator.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionComparator.java @@ -13,9 +13,9 @@ public class LoanTransactionComparator implements Comparator { 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; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionType.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionType.java index 1e9e51876..7a1312ec8 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionType.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionType.java @@ -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; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/MifosStyleLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/MifosStyleLoanRepaymentScheduleTransactionProcessor.java index 2a2834dab..be0b9e407 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/MifosStyleLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/MifosStyleLoanRepaymentScheduleTransactionProcessor.java @@ -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; } diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/RBILoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/RBILoanRepaymentScheduleTransactionProcessor.java index 6422eb217..10caf1e70 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/RBILoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/RBILoanRepaymentScheduleTransactionProcessor.java @@ -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 { diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanReadPlatformServiceImpl.java index 5acbb9126..0ff1b8535 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanReadPlatformServiceImpl.java @@ -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); diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformService.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformService.java index 224efb8ec..a7b359290 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformService.java @@ -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); } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 175a29255..8e49b006d 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -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 diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loanproduct/service/LoanEnumerations.java b/mifosng-provider/src/main/java/org/mifosng/platform/loanproduct/service/LoanEnumerations.java index c561142b6..f615ee46c 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loanproduct/service/LoanEnumerations.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loanproduct/service/LoanEnumerations.java @@ -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; }