diff --git a/mifosng-db/migrations/core_db/V128__added_loan_installment_charge.sql b/mifosng-db/migrations/core_db/V128__added_loan_installment_charge.sql new file mode 100755 index 000000000..ed95e725e --- /dev/null +++ b/mifosng-db/migrations/core_db/V128__added_loan_installment_charge.sql @@ -0,0 +1,19 @@ +CREATE TABLE `m_loan_installment_charge` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `loan_charge_id` BIGINT(20) NOT NULL, + `loan_schedule_id` BIGINT(20) NOT NULL, + `due_date` DATE NULL DEFAULT NULL, + `amount` DECIMAL(19,6) NOT NULL, + `amount_paid_derived` DECIMAL(19,6) NULL DEFAULT NULL, + `amount_waived_derived` DECIMAL(19,6) NULL DEFAULT NULL, + `amount_writtenoff_derived` DECIMAL(19,6) NULL DEFAULT NULL, + `amount_outstanding_derived` DECIMAL(19,6) NOT NULL DEFAULT '0.000000', + `is_paid_derived` TINYINT(1) NOT NULL DEFAULT '0', + `waived` TINYINT(1) NOT NULL DEFAULT '0', + `amount_through_charge_payment` DECIMAL(19,6) NULL DEFAULT NULL, + PRIMARY KEY (`id`), + CONSTRAINT `FK_loan_charge_id_charge_schedule` FOREIGN KEY (`loan_charge_id`) REFERENCES `m_loan_charge` (`id`), + CONSTRAINT `FK_loan_schedule_id_charge_schedule` FOREIGN KEY (`loan_schedule_id`) REFERENCES `m_loan_repayment_schedule` (`id`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/data/AccountTransferDTO.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/data/AccountTransferDTO.java index fd835b15e..0233e7ef4 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/data/AccountTransferDTO.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/data/AccountTransferDTO.java @@ -23,11 +23,12 @@ public class AccountTransferDTO { private final Integer fromTransferType; private final Integer toTransferType; private final Long chargeId; + private final Integer loanInstallmentNumber; public AccountTransferDTO(final LocalDate transactionDate, final BigDecimal transactionAmount, final PortfolioAccountType fromAccountType, final PortfolioAccountType toAccountType, final Long fromAccountId, final Long toAccountId, final String description, final Locale locale, final DateTimeFormatter fmt, - final PaymentDetail paymentDetail, final Integer fromTransferType, final Integer toTransferType, final Long chargeId) { + final PaymentDetail paymentDetail, final Integer fromTransferType, final Integer toTransferType, final Long chargeId, Integer loanInstallmentNumber) { this.transactionDate = transactionDate; this.transactionAmount = transactionAmount; this.fromAccountType = fromAccountType; @@ -41,6 +42,7 @@ public class AccountTransferDTO { this.fromTransferType = fromTransferType; this.toTransferType = toTransferType; this.chargeId = chargeId; + this.loanInstallmentNumber = loanInstallmentNumber; } public LocalDate getTransactionDate() { @@ -95,4 +97,9 @@ public class AccountTransferDTO { return this.chargeId; } + + public Integer getLoanInstallmentNumber() { + return this.loanInstallmentNumber; + } + } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java index 029fd2e74..799f7b023 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/account/service/AccountTransfersWritePlatformServiceImpl.java @@ -229,7 +229,7 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer final LoanTransaction loanRepaymentTransaction = this.loanAccountDomainService.makeChargePayment(toLoanAccount, accountTransferDTO.getChargeId(), accountTransferDTO.getTransactionDate(), accountTransferDTO.getTransactionAmount(), - accountTransferDTO.getPaymentDetail(), null, null, accountTransferDTO.getToTransferType()); + accountTransferDTO.getPaymentDetail(), null, null, accountTransferDTO.getToTransferType(), accountTransferDTO.getLoanInstallmentNumber()); final AccountTransfer transferTransaction = this.accountTransferAssembler.assembleSavingsToLoanTransfer(accountTransferDTO, fromSavingsAccount, toLoanAccount, withdrawal, loanRepaymentTransaction); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanChargesApiResource.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanChargesApiResource.java index ec18e5165..48d42e70c 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanChargesApiResource.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/api/LoanChargesApiResource.java @@ -36,6 +36,7 @@ import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext import org.mifosplatform.portfolio.charge.data.ChargeData; import org.mifosplatform.portfolio.charge.service.ChargeReadPlatformService; import org.mifosplatform.portfolio.loanaccount.data.LoanChargeData; +import org.mifosplatform.portfolio.loanaccount.data.LoanInstallmentChargeData; import org.mifosplatform.portfolio.loanaccount.service.LoanChargeReadPlatformService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; @@ -48,7 +49,7 @@ public class LoanChargesApiResource { private final Set RESPONSE_DATA_PARAMETERS = new HashSet(Arrays.asList("id", "chargeId", "name", "penalty", "chargeTimeType", "dueAsOfDate", "chargeCalculationType", "percentage", "amountPercentageAppliedTo", "currency", - "amountWaived", "amountWrittenOff", "amountOutstanding", "amountOrPercentage", "amount", "amountPaid", "chargeOptions")); + "amountWaived", "amountWrittenOff", "amountOutstanding", "amountOrPercentage", "amount", "amountPaid", "chargeOptions","installmentChargeData")); private final String resourceNameForPermissions = "LOAN"; @@ -116,9 +117,13 @@ public class LoanChargesApiResource { this.context.authenticatedUser().validateHasReadPermission(this.resourceNameForPermissions); final LoanChargeData loanCharge = this.loanChargeReadPlatformService.retrieveLoanChargeDetails(loanChargeId, loanId); + + final Collection installmentChargeData = this.loanChargeReadPlatformService.retrieveInstallmentLoanCharges(loanChargeId, true); + + final LoanChargeData loanChargeData = new LoanChargeData(loanCharge, installmentChargeData); final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters()); - return this.toApiJsonSerializer.serialize(settings, loanCharge, this.RESPONSE_DATA_PARAMETERS); + return this.toApiJsonSerializer.serialize(settings, loanChargeData, this.RESPONSE_DATA_PARAMETERS); } @POST diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanChargeData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanChargeData.java index f7221991b..bbecea402 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanChargeData.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanChargeData.java @@ -13,6 +13,7 @@ import org.mifosplatform.infrastructure.core.data.EnumOptionData; import org.mifosplatform.organisation.monetary.data.CurrencyData; import org.mifosplatform.portfolio.charge.data.ChargeData; import org.mifosplatform.portfolio.charge.domain.ChargePaymentMode; +import org.mifosplatform.portfolio.charge.domain.ChargeTimeType; /** * Immutable data object for loan charge data. @@ -20,45 +21,32 @@ import org.mifosplatform.portfolio.charge.domain.ChargePaymentMode; public class LoanChargeData { private final Long id; - @SuppressWarnings("unused") private final Long chargeId; - @SuppressWarnings("unused") private final String name; - @SuppressWarnings("unused") private final EnumOptionData chargeTimeType; private final LocalDate dueDate; - @SuppressWarnings("unused") private final EnumOptionData chargeCalculationType; private final BigDecimal percentage; - @SuppressWarnings("unused") private final BigDecimal amountPercentageAppliedTo; - @SuppressWarnings("unused") private final CurrencyData currency; - @SuppressWarnings("unused") private final BigDecimal amount; - @SuppressWarnings("unused") private final BigDecimal amountPaid; - @SuppressWarnings("unused") private final BigDecimal amountWaived; - @SuppressWarnings("unused") private final BigDecimal amountWrittenOff; private final BigDecimal amountOutstanding; - @SuppressWarnings("unused") private final BigDecimal amountOrPercentage; - @SuppressWarnings("unused") private final Collection chargeOptions; - @SuppressWarnings("unused") private final boolean penalty; private final EnumOptionData chargePaymentMode; @@ -67,20 +55,20 @@ public class LoanChargeData { private final boolean waived; - @SuppressWarnings("unused") private final boolean chargePayable; private final Long loanId; - @SuppressWarnings("unused") private final BigDecimal minCap; - @SuppressWarnings("unused") private final BigDecimal maxCap; + + @SuppressWarnings("unused") + private final Collection installmentChargeData; public static LoanChargeData template(final Collection chargeOptions) { return new LoanChargeData(null, null, null, null, null, null, null, null, chargeOptions, false, null, false, false, null, null, - null,null); + null,null,null); } /** @@ -92,7 +80,7 @@ public class LoanChargeData { final EnumOptionData chargeCalculationType, final boolean penalty, final EnumOptionData chargePaymentMode, final BigDecimal minCap, final BigDecimal maxCap) { return new LoanChargeData(null, chargeId, name, currency, amount, percentage, chargeTimeType, chargeCalculationType, null, penalty, - chargePaymentMode, false, false, null, minCap, maxCap,null); + chargePaymentMode, false, false, null, minCap, maxCap,null, null); } public LoanChargeData(final Long id, final Long chargeId, final String name, final CurrencyData currency, final BigDecimal amount, @@ -100,7 +88,7 @@ public class LoanChargeData { final BigDecimal amountOutstanding, final EnumOptionData chargeTimeType, final LocalDate dueDate, final EnumOptionData chargeCalculationType, final BigDecimal percentage, final BigDecimal amountPercentageAppliedTo, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid, final boolean waived, final Long loanId, - final BigDecimal minCap, final BigDecimal maxCap,final BigDecimal amountOrPercentage) { + final BigDecimal minCap, final BigDecimal maxCap,final BigDecimal amountOrPercentage, Collection installmentChargeData) { this.id = id; this.chargeId = chargeId; this.name = name; @@ -134,12 +122,13 @@ public class LoanChargeData { this.chargeOptions = null; this.chargePayable = isChargePayable(); this.loanId = loanId; + this.installmentChargeData = installmentChargeData; } private LoanChargeData(final Long id, final Long chargeId, final String name, final CurrencyData currency, final BigDecimal amount, final BigDecimal percentage, final EnumOptionData chargeTimeType, final EnumOptionData chargeCalculationType, final Collection chargeOptions, final boolean penalty, final EnumOptionData chargePaymentMode, final boolean paid, - final boolean waived, final Long loanId, final BigDecimal minCap, final BigDecimal maxCap,final BigDecimal amountOrPercentage) { + final boolean waived, final Long loanId, final BigDecimal minCap, final BigDecimal maxCap,final BigDecimal amountOrPercentage, Collection installmentChargeData) { this.id = id; this.chargeId = chargeId; this.name = name; @@ -174,9 +163,10 @@ public class LoanChargeData { this.loanId = loanId; this.minCap = minCap; this.maxCap = maxCap; + this.installmentChargeData = installmentChargeData; } - public LoanChargeData(final Long id, final LocalDate dueAsOfDate, final BigDecimal amountOutstanding, final Long loanId) { + public LoanChargeData(final Long id, final LocalDate dueAsOfDate, final BigDecimal amountOutstanding, EnumOptionData chargeTimeType, final Long loanId, Collection installmentChargeData) { this.id = id; this.chargeId = null; this.name = null; @@ -186,7 +176,7 @@ public class LoanChargeData { this.amountWaived = null; this.amountWrittenOff = null; this.amountOutstanding = amountOutstanding; - this.chargeTimeType = null; + this.chargeTimeType = chargeTimeType; this.dueDate = dueAsOfDate; this.chargeCalculationType = null; this.percentage = null; @@ -201,7 +191,37 @@ public class LoanChargeData { this.loanId = loanId; this.minCap = null; this.maxCap = null; + this.installmentChargeData = installmentChargeData; } + + public LoanChargeData(LoanChargeData chargeData, Collection installmentChargeData) { + this.id = chargeData.id; + this.chargeId = chargeData.chargeId; + this.name = chargeData.name; + this.currency = chargeData.currency; + this.amount = chargeData.amount; + this.amountPaid = chargeData.amountPaid; + this.amountWaived = chargeData.amountWaived; + this.amountWrittenOff = chargeData.amountWrittenOff; + this.amountOutstanding = chargeData.amountOutstanding; + this.chargeTimeType = chargeData.chargeTimeType; + this.dueDate = chargeData.dueDate; + this.chargeCalculationType = chargeData.chargeCalculationType; + this.percentage = chargeData.percentage; + this.amountPercentageAppliedTo = chargeData.amountPercentageAppliedTo; + this.penalty = chargeData.penalty; + this.chargePaymentMode = chargeData.chargePaymentMode; + this.paid = chargeData.paid; + this.waived = chargeData.waived; + this.minCap = chargeData.minCap; + this.maxCap = chargeData.maxCap; + this.amountOrPercentage = chargeData.amountOrPercentage; + this.chargeOptions = chargeData.chargeOptions; + this.chargePayable = chargeData.chargePayable; + this.loanId = chargeData.loanId; + this.installmentChargeData = installmentChargeData; + } + public boolean isChargePayable() { boolean isAccountTransfer = false; @@ -226,4 +246,12 @@ public class LoanChargeData { public BigDecimal getAmountOutstanding() { return this.amountOutstanding; } + + public boolean isInstallmentFee() { + boolean isInstalmentFee = false; + if (this.chargeTimeType != null) { + isInstalmentFee = ChargeTimeType.fromInt(this.chargeTimeType.getId().intValue()).isInstalmentFee(); + } + return isInstalmentFee; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanInstallmentChargeData.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanInstallmentChargeData.java new file mode 100755 index 000000000..1ba88c000 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/data/LoanInstallmentChargeData.java @@ -0,0 +1,46 @@ +package org.mifosplatform.portfolio.loanaccount.data; + +import java.math.BigDecimal; + +import org.joda.time.LocalDate; + +public class LoanInstallmentChargeData { + + private final Integer installmentNumber; + private final LocalDate dueDate; + @SuppressWarnings("unused") + private final BigDecimal amount; + @SuppressWarnings("unused") + private final BigDecimal amountOutstanding; + private final boolean paid; + private final boolean waived; + + public LoanInstallmentChargeData(final Integer installmentNumber, final LocalDate dueDate, final BigDecimal amount, + final BigDecimal amountOutstanding, final boolean paid, final boolean waived) { + this.installmentNumber = installmentNumber; + this.dueDate = dueDate; + this.amount = amount; + this.amountOutstanding = amountOutstanding; + this.paid = paid; + this.waived = waived; + } + + public Integer getInstallmentNumber() { + return this.installmentNumber; + } + + public LocalDate getDueDate() { + return this.dueDate; + } + + public boolean isPaymentPending(){ + return !(paid || waived); + } + + + public BigDecimal getAmountOutstanding() { + return this.amountOutstanding; + } + + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java index fb82a6859..d731ee217 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/Loan.java @@ -520,7 +520,7 @@ public class Loan extends AbstractPersistable { } private void handleChargePaidTransaction(final LoanCharge charge, final LoanTransaction chargesPayment, - final LoanLifecycleStateMachine loanLifecycleStateMachine) { + final LoanLifecycleStateMachine loanLifecycleStateMachine, Integer installmentNumber) { chargesPayment.updateLoan(this); final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, chargesPayment.getAmount(getCurrency()) .getAmount()); @@ -535,7 +535,10 @@ public class Loan extends AbstractPersistable { final List chargePaymentInstallments = new ArrayList(); LocalDate startDate = getDisbursementDate(); for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { - if (charge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate())) { + if (installmentNumber == null && charge.isDueForCollectionFromAndUpToAndIncluding(startDate, installment.getDueDate())) { + chargePaymentInstallments.add(installment); + break; + }else if(installmentNumber != null && installment.getInstallmentNumber().equals(installmentNumber)){ chargePaymentInstallments.add(installment); break; } @@ -708,31 +711,45 @@ public class Loan extends AbstractPersistable { public BigDecimal calculatePerInstallmentChargeAmount(final ChargeCalculationType calculationType, final BigDecimal percentage) { Money amount = Money.zero(getCurrency()); for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { - Money percentOf = Money.zero(getCurrency()); - switch (calculationType) { - case PERCENT_OF_AMOUNT: - percentOf = installment.getPrincipal(getCurrency()); - break; - case PERCENT_OF_AMOUNT_AND_INTEREST: - percentOf = installment.getPrincipal(getCurrency()).plus(installment.getInterestCharged(getCurrency())); - break; - case PERCENT_OF_INTEREST: - percentOf = installment.getInterestCharged(getCurrency()); - break; - default: - break; - } - amount = amount.plus(LoanCharge.percentageOf(percentOf.getAmount(), percentage)); + amount = amount.plus(calculateInstallmentChargeAmount(calculationType, percentage, installment)); } return amount.getAmount(); } + /** + * @param calculationType + * @param percentage + * @param installment + * @return + */ + private Money calculateInstallmentChargeAmount(final ChargeCalculationType calculationType, final BigDecimal percentage, + final LoanRepaymentScheduleInstallment installment) { + Money amount = Money.zero(getCurrency()); + Money percentOf = Money.zero(getCurrency()); + switch (calculationType) { + case PERCENT_OF_AMOUNT: + percentOf = installment.getPrincipal(getCurrency()); + break; + case PERCENT_OF_AMOUNT_AND_INTEREST: + percentOf = installment.getPrincipal(getCurrency()).plus(installment.getInterestCharged(getCurrency())); + break; + case PERCENT_OF_INTEREST: + percentOf = installment.getInterestCharged(getCurrency()); + break; + default: + break; + } + amount = amount.plus(LoanCharge.percentageOf(percentOf.getAmount(), percentage)); + return amount; + } + public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLifecycleStateMachine loanLifecycleStateMachine, - final Map changes, final List existingTransactionIds, final List existingReversedTransactionIds) { + final Map changes, final List existingTransactionIds, final List existingReversedTransactionIds, + Integer loanInstallmentNumber) { validateLoanIsNotClosed(loanCharge); - final Money amountWaived = loanCharge.waive(loanCurrency()); + final Money amountWaived = loanCharge.waive(loanCurrency(), loanInstallmentNumber); changes.put("amount", amountWaived.getAmount()); @@ -868,8 +885,9 @@ public class Loan extends AbstractPersistable { } public void updateLoanSchedule(final LoanScheduleModel modifiedLoanSchedule) { + clearInstallmentLoanCharge(); this.repaymentScheduleInstallments.clear(); - + for (final LoanScheduleModelPeriod scheduledLoanInstallment : modifiedLoanSchedule.getPeriods()) { if (scheduledLoanInstallment.isRepaymentPeriod()) { @@ -884,6 +902,15 @@ public class Loan extends AbstractPersistable { updateLoanScheduleDependentDerivedFields(); updateLoanSummaryDerivedFields(); + + } + + private void clearInstallmentLoanCharge(){ + for(LoanCharge loanCharge:this.charges){ + if(loanCharge.isInstalmentFee()){ + loanCharge.clearLoanInstallmentCharges(); + } + } } private void updateLoanScheduleDependentDerivedFields() { @@ -1723,7 +1750,7 @@ public class Loan extends AbstractPersistable { public void makeChargePayment(final Long chargeId, final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, final List existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List holidays, final WorkingDays workingDays, - final boolean allowTransactionsOnNonWorkingDay, final LoanTransaction paymentTransaction) { + final boolean allowTransactionsOnNonWorkingDay, final LoanTransaction paymentTransaction, Integer installmentNumber) { validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_CHARGE_PAYMENT, paymentTransaction.getTransactionDate()); validateRepaymentDateIsOnHoliday(paymentTransaction.getTransactionDate(), allowTransactionsOnHoliday, holidays); @@ -1742,7 +1769,7 @@ public class Loan extends AbstractPersistable { charge = loanCharge; } } - handleChargePaidTransaction(charge, paymentTransaction, loanLifecycleStateMachine); + handleChargePaidTransaction(charge, paymentTransaction, loanLifecycleStateMachine, installmentNumber); } public void makeRefund(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine, @@ -2961,4 +2988,22 @@ public class Loan extends AbstractPersistable { public Set charges() { return this.charges; } + + public Set generateInstallmentLoanCharges(final LoanCharge loanCharge){ + Set loanChargePerInstallments = new HashSet(); + if(loanCharge.isInstalmentFee()){ + for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { + BigDecimal amount = BigDecimal.ZERO; + if(loanCharge.getChargeCalculation().isFlat()){ + amount = loanCharge.amount().divide(BigDecimal.valueOf(repaymentScheduleDetail().getNumberOfRepayments())); + }else{ + amount = calculateInstallmentChargeAmount(loanCharge.getChargeCalculation(), loanCharge.getPercentage(), installment).getAmount(); + } + LoanInstallmentCharge loanInstallmentCharge = new LoanInstallmentCharge(amount, loanCharge, installment); + loanChargePerInstallments.add(loanInstallmentCharge); + } + } + return loanChargePerInstallments; + } + } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java index 8d6d6a742..0e2f1d9de 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainService.java @@ -22,5 +22,5 @@ public interface LoanAccountDomainService { void reverseTransfer(LoanTransaction loanTransaction); LoanTransaction makeChargePayment(Loan loan, Long chargeId, LocalDate transactionDate, BigDecimal transactionAmount, - PaymentDetail paymentDetail, String noteText, String txnExternalId, Integer transactionType); + PaymentDetail paymentDetail, String noteText, String txnExternalId, Integer transactionType, Integer installmentNumber); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index fa75c87fa..315c73bed 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -163,7 +163,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { @Transactional public LoanTransaction makeChargePayment(final Loan loan, final Long chargeId, final LocalDate transactionDate, final BigDecimal transactionAmount, final PaymentDetail paymentDetail, final String noteText, final String txnExternalId, - final Integer transactionType) { + final Integer transactionType, Integer installmentNumber) { checkClientOrGroupActive(loan); @@ -184,7 +184,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { loan.handlePayDisbursementTransaction(chargeId, newPaymentTransaction, existingTransactionIds, existingReversedTransactionIds); } else { loan.makeChargePayment(chargeId, defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds, - allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay, newPaymentTransaction); + allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay, newPaymentTransaction, installmentNumber); } this.loanTransactionRepository.save(newPaymentTransaction); this.loanRepository.saveAndFlush(loan); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java index c615ae627..9d99eccf3 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanCharge.java @@ -8,20 +8,27 @@ package org.mifosplatform.portfolio.loanaccount.domain; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; +import java.util.Collection; import java.util.Date; +import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.api.JsonCommand; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; @@ -65,6 +72,7 @@ public class LoanCharge extends AbstractPersistable { @Column(name = "calculation_on_amount", scale = 6, precision = 19, nullable = true) private BigDecimal amountPercentageAppliedTo; + @SuppressWarnings("unused") @Column(name = "charge_amount_or_percentage", scale = 6, precision = 19, nullable = false) private BigDecimal amountOrPercentage; @@ -97,6 +105,10 @@ public class LoanCharge extends AbstractPersistable { @Column(name = "max_cap", scale = 6, precision = 19, nullable = true) private BigDecimal maxCap; + + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "loancharge", orphanRemoval = true) + private Set loanInstallmentCharge = new HashSet(); public static LoanCharge createNewFromJson(final Loan loan, final Charge chargeDefinition, final JsonCommand command) { @@ -236,6 +248,19 @@ public class LoanCharge extends AbstractPersistable { break; } this.amountOrPercentage = chargeAmount; + if(this.loan!=null && this.isInstalmentFee()){ + Set chargePerInstallments = this.loan.generateInstallmentLoanCharges(this); + if(this.loanInstallmentCharge.isEmpty()){ + this.loanInstallmentCharge.addAll(chargePerInstallments); + }else{ + int index = 0; + LoanInstallmentCharge[] loanChargePerInstallments = new LoanInstallmentCharge[chargePerInstallments.size()]; + LoanInstallmentCharge[] loanChargePerInstallmentArray = chargePerInstallments.toArray(loanChargePerInstallments); + for(LoanInstallmentCharge chargePerInstallment:this.loanInstallmentCharge){ + chargePerInstallment.copyFrom(loanChargePerInstallmentArray[index++]); + } + } + } } public void markAsFullyPaid() { @@ -251,20 +276,41 @@ public class LoanCharge extends AbstractPersistable { this.amountOutstanding = calculateAmountOutstanding(currency); this.paid = false; this.waived = false; + for(LoanInstallmentCharge installmentCharge:loanInstallmentCharge){ + installmentCharge.resetPaidAmount(currency); + } } public void resetPaidAmount(final MonetaryCurrency currency) { this.amountPaid = BigDecimal.ZERO; this.amountOutstanding = calculateAmountOutstanding(currency); this.paid = false; + for(LoanInstallmentCharge installmentCharge:loanInstallmentCharge){ + installmentCharge.resetPaidAmount(currency); + } } - public Money waive(final MonetaryCurrency currency) { + public Money waive(final MonetaryCurrency currency, Integer loanInstallmentNumber) { + if(isInstalmentFee()){ + LoanInstallmentCharge chargePerInstallment = getInstallmentLoanCharge(loanInstallmentNumber); + Money amountWaived = chargePerInstallment.waive(currency); + if(this.amountWaived == null){ + this.amountWaived = BigDecimal.ZERO; + } + this.amountWaived = this.amountWaived.add(amountWaived.getAmount()); + this.amountOutstanding = this.amountOutstanding.subtract(amountWaived.getAmount()); + if(determineIfFullyPaid()){ + this.paid = false; + this.waived = true; + } + return amountWaived; + } this.amountWaived = this.amountOutstanding; this.amountOutstanding = BigDecimal.ZERO; this.paid = false; this.waived = true; return getAmountWaived(currency); + } private BigDecimal calculateAmountOutstanding(final MonetaryCurrency currency) { @@ -307,6 +353,19 @@ public class LoanCharge extends AbstractPersistable { break; } this.amountOrPercentage = amount; + if(this.loan !=null && this.isInstalmentFee()){ + Set chargePerInstallments = this.loan.generateInstallmentLoanCharges(this); + if(this.loanInstallmentCharge.isEmpty()){ + this.loanInstallmentCharge.addAll(chargePerInstallments); + }else{ + int index = 0; + LoanInstallmentCharge[] loanChargePerInstallments = new LoanInstallmentCharge[chargePerInstallments.size()]; + LoanInstallmentCharge[] loanChargePerInstallmentArray = chargePerInstallments.toArray(loanChargePerInstallments); + for(LoanInstallmentCharge chargePerInstallment:this.loanInstallmentCharge){ + chargePerInstallment.copyFrom(loanChargePerInstallmentArray[index++]); + } + } + } } } @@ -357,6 +416,19 @@ public class LoanCharge extends AbstractPersistable { break; } this.amountOrPercentage = newValue; + if(this.isInstalmentFee()){ + Set chargePerInstallments = this.loan.generateInstallmentLoanCharges(this); + if(this.loanInstallmentCharge.isEmpty()){ + this.loanInstallmentCharge.addAll(chargePerInstallments); + }else{ + int index = 0; + LoanInstallmentCharge[] loanChargePerInstallments = new LoanInstallmentCharge[chargePerInstallments.size()]; + LoanInstallmentCharge[] loanChargePerInstallmentArray = chargePerInstallments.toArray(loanChargePerInstallments); + for(LoanInstallmentCharge chargePerInstallment:this.loanInstallmentCharge){ + chargePerInstallment.copyFrom(loanChargePerInstallmentArray[index++]); + } + } + } } return actualChanges; } @@ -538,28 +610,36 @@ public class LoanCharge extends AbstractPersistable { } /** - * @param incrementBy + * @param feeAmount TODO + * @param processAmount * Amount used to pay off this charge * @return Actual amount paid on this charge */ - public Money updatePaidAmountBy(final Money incrementBy) { + public Money updatePaidAmountBy(final Money incrementBy,final Integer installmentNumber, Money feeAmount) { + Money processAmount = Money.zero(incrementBy.getCurrency()); + if(this.isInstalmentFee()){ + if(installmentNumber == null){ + processAmount = getUnpaidInstallmentLoanCharge().updatePaidAmountBy(incrementBy, feeAmount); + }else{ + processAmount = getInstallmentLoanCharge(installmentNumber).updatePaidAmountBy(incrementBy, feeAmount); + } + }else{ + processAmount = incrementBy; + } + Money amountPaidToDate = Money.of(processAmount.getCurrency(), this.amountPaid); + final Money amountOutstanding = Money.of(processAmount.getCurrency(), this.amountOutstanding); - Money amountPaidToDate = Money.of(incrementBy.getCurrency(), this.amountPaid); - final Money amountOutstanding = Money.of(incrementBy.getCurrency(), this.amountOutstanding); - - Money amountPaidOnThisCharge = Money.zero(incrementBy.getCurrency()); - if (incrementBy.isGreaterThanOrEqualTo(amountOutstanding)) { + Money amountPaidOnThisCharge = Money.zero(processAmount.getCurrency()); + if (processAmount.isGreaterThanOrEqualTo(amountOutstanding)) { amountPaidOnThisCharge = amountOutstanding; amountPaidToDate = amountPaidToDate.plus(amountOutstanding); this.amountPaid = amountPaidToDate.getAmount(); this.amountOutstanding = BigDecimal.ZERO; } else { - amountPaidOnThisCharge = incrementBy; - amountPaidToDate = amountPaidToDate.plus(incrementBy); + amountPaidOnThisCharge = processAmount; + amountPaidToDate = amountPaidToDate.plus(processAmount); this.amountPaid = amountPaidToDate.getAmount(); - - final Money amountExpected = Money.of(incrementBy.getCurrency(), this.amount); - this.amountOutstanding = amountExpected.minus(amountPaidToDate).getAmount(); + this.amountOutstanding = calculateAmountOutstanding(incrementBy.getCurrency()); } this.paid = determineIfFullyPaid(); @@ -606,19 +686,68 @@ public class LoanCharge extends AbstractPersistable { return ChargePaymentMode.fromInt(this.chargePaymentMode); } - public ChargeCalculationType getChargeCalculation() { return ChargeCalculationType.fromInt(this.chargeCalculation); } - public BigDecimal getPercentage() { return this.percentage; } - public void updateAmount(final BigDecimal amount) { this.amount = amount; calculateOutstanding(); } + + public LoanInstallmentCharge getUnpaidInstallmentLoanCharge() { + LoanInstallmentCharge unpaidChargePerInstallment = null; + for (LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) { + if (loanChargePerInstallment.isPending() + && (unpaidChargePerInstallment == null || unpaidChargePerInstallment.getRepaymentInstallment().getDueDate() + .isAfter(loanChargePerInstallment.getRepaymentInstallment().getDueDate()))) { + unpaidChargePerInstallment = loanChargePerInstallment; + } + } + return unpaidChargePerInstallment; + } + + public LoanInstallmentCharge getInstallmentLoanCharge(LocalDate periodDueDate) { + for (LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) { + if (periodDueDate.isEqual(loanChargePerInstallment.getRepaymentInstallment().getDueDate())) { + return loanChargePerInstallment; + } + } + return null; + } + + public LoanInstallmentCharge getInstallmentLoanCharge(Integer installmentNumber) { + for (LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) { + if (installmentNumber.equals(loanChargePerInstallment.getRepaymentInstallment().getInstallmentNumber().intValue()) ){ + return loanChargePerInstallment; + } + } + return null; + } + + public void clearLoanInstallmentCharges(){ + this.loanInstallmentCharge.clear(); + } + + public void addLoanInstallmentCharges(Collection installmentCharges){ + this.loanInstallmentCharge.addAll(installmentCharges); + } + + public boolean hasNoLoanInstallmentCharges(){ + return this.loanInstallmentCharge.isEmpty(); + } + + public LoanRepaymentScheduleInstallment fetchRepaymentInstallment(Money trasferedAmount){ + for (LoanInstallmentCharge loanChargePerInstallment : this.loanInstallmentCharge) { + if (loanChargePerInstallment.isPending() && + trasferedAmount.getAmount().equals(loanChargePerInstallment.getAmountThroughChargePayment(trasferedAmount.getCurrency()).getAmount()) ){ + return loanChargePerInstallment.getRepaymentInstallment(); + } + } + return null; + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanInstallmentCharge.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanInstallmentCharge.java new file mode 100755 index 000000000..6adf43d43 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanInstallmentCharge.java @@ -0,0 +1,211 @@ +/** + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.mifosplatform.portfolio.loanaccount.domain; + +import java.math.BigDecimal; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; +import org.mifosplatform.organisation.monetary.domain.Money; +import org.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_loan_installment_charge") +public class LoanInstallmentCharge extends AbstractPersistable { + + @SuppressWarnings("unused") + @ManyToOne(optional = false) + @JoinColumn(name = "loan_charge_id", referencedColumnName = "id", nullable = false) + private LoanCharge loancharge; + + @ManyToOne(optional = false) + @JoinColumn(name = "loan_schedule_id", referencedColumnName = "id", nullable = false) + private LoanRepaymentScheduleInstallment installment; + + @Column(name = "amount", scale = 6, precision = 19, nullable = false) + private BigDecimal amount; + + @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; + + @Column(name = "amount_through_charge_payment", scale = 6, precision = 19, nullable = true) + private BigDecimal amountThroughChargePayment; + + @Column(name = "is_paid_derived", nullable = false) + private boolean paid = false; + + @Column(name = "waived", nullable = false) + private boolean waived = false; + + + public LoanInstallmentCharge() { + // TODO Auto-generated constructor stub + } + + public LoanInstallmentCharge(BigDecimal amount,LoanCharge loanCharge,LoanRepaymentScheduleInstallment installment) { + this.loancharge = loanCharge; + this.installment = installment; + this.amount = amount; + this.amountOutstanding = amount; + this.amountPaid = null; + this.amountWaived = null; + this.amountWrittenOff = null; + } + + public void copyFrom(LoanInstallmentCharge loanChargePerInstallment){ + this.amount = loanChargePerInstallment.amount; + this.installment = loanChargePerInstallment.installment; + this.amountOutstanding = calculateOutstanding(); + this.paid = determineIfFullyPaid(); + } + + + public Money waive(final MonetaryCurrency currency) { + this.amountWaived = this.amountOutstanding; + this.amountOutstanding = BigDecimal.ZERO; + this.paid = false; + this.waived = true; + return getAmountWaived(currency); + } + + + public Money getAmountWaived(final MonetaryCurrency currency) { + return Money.of(currency, this.amountWaived); + } + + private boolean determineIfFullyPaid() { + if(this.amount == null){ + return true; + } + return BigDecimal.ZERO.compareTo(calculateOutstanding()) == 0; + } + + private BigDecimal calculateOutstanding() { + if(this.amount == null){ + return null; + } + BigDecimal amountPaidLocal = BigDecimal.ZERO; + if (this.amountPaid != null) { + amountPaidLocal = this.amountPaid; + } + + BigDecimal amountWaivedLocal = BigDecimal.ZERO; + if (this.amountWaived != null) { + amountWaivedLocal = this.amountWaived; + } + + BigDecimal amountWrittenOffLocal = BigDecimal.ZERO; + if (this.amountWrittenOff != null) { + amountWrittenOffLocal = this.amountWrittenOff; + } + + final BigDecimal totalAccountedFor = amountPaidLocal.add(amountWaivedLocal).add(amountWrittenOffLocal); + + return this.amount.subtract(totalAccountedFor); + } + + + public BigDecimal getAmount() { + return this.amount; + } + + public Money getAmount(final MonetaryCurrency currency) { + return Money.of(currency, this.amount); + } + + private Money getAmountPaid(final MonetaryCurrency currency) { + return Money.of(currency, this.amountPaid); + } + + public BigDecimal getAmountOutstanding() { + return this.amountOutstanding; + } + + private BigDecimal calculateAmountOutstanding(final MonetaryCurrency currency) { + return getAmount(currency).minus(getAmountWaived(currency)).minus(getAmountPaid(currency)).getAmount(); + } + + + public boolean isPaid() { + return this.paid; + } + + public boolean isWaived() { + return this.waived; + } + + public boolean isPending(){ + return !(isPaid() || isWaived()); + } + + + public LoanRepaymentScheduleInstallment getRepaymentInstallment() { + return this.installment; + } + + public Money updatePaidAmountBy(final Money incrementBy, Money feeAmount) { + + Money amountPaidToDate = Money.of(incrementBy.getCurrency(), this.amountPaid); + final Money amountOutstanding = Money.of(incrementBy.getCurrency(), this.amountOutstanding); + + Money amountPaidOnThisCharge = Money.zero(incrementBy.getCurrency()); + if (incrementBy.isGreaterThanOrEqualTo(amountOutstanding)) { + amountPaidOnThisCharge = amountOutstanding; + amountPaidToDate = amountPaidToDate.plus(amountOutstanding); + this.amountPaid = amountPaidToDate.getAmount(); + this.amountOutstanding = BigDecimal.ZERO; + } else { + amountPaidOnThisCharge = incrementBy; + amountPaidToDate = amountPaidToDate.plus(incrementBy); + this.amountPaid = amountPaidToDate.getAmount(); + this.amountOutstanding = calculateAmountOutstanding(incrementBy.getCurrency()); + } + this.amountThroughChargePayment = feeAmount.getAmount(); + this.paid = determineIfFullyPaid(); + + return amountPaidOnThisCharge; + } + + + public Money getAmountWrittenOff(final MonetaryCurrency currency) { + return Money.of(currency, this.amountWrittenOff); + } + + public void resetPaidAmount(final MonetaryCurrency currency) { + this.amountPaid = BigDecimal.ZERO; + this.amountOutstanding = calculateAmountOutstanding(currency); + this.paid = false; + } + + 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; + this.waived = false; + } + + + public Money getAmountThroughChargePayment(final MonetaryCurrency currency) { + return Money.of(currency, this.amountThroughChargePayment); + } + +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index abe5b0c32..5d1464966 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -7,15 +7,22 @@ package org.mifosplatform.portfolio.loanaccount.domain; import java.math.BigDecimal; import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.FetchType; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; +import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; import org.joda.time.LocalDate; import org.mifosplatform.infrastructure.core.domain.AbstractAuditableCustom; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; @@ -98,7 +105,7 @@ public final class LoanRepaymentScheduleInstallment extends AbstractAuditableCus @Temporal(TemporalType.DATE) @Column(name = "obligations_met_on_date") private Date obligationsMetOnDate; - + protected LoanRepaymentScheduleInstallment() { this.installmentNumber = null; this.fromDate = null; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java index 2ef229dd4..058693113 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanRepaymentScheduleProcessingWrapper.java @@ -70,7 +70,8 @@ public class LoanRepaymentScheduleProcessingWrapper { } else { cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments))); } - } else if (loanCharge.getChargeCalculation().isPercentageBased()) { + } else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && + loanCharge.getChargeCalculation().isPercentageBased()) { BigDecimal amount = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { amount = amount.add(period.getLoan().getSummary().getTotalPrincipalDisbursed()).add(period.getLoan().getSummary().getTotalInterestCharged()); @@ -96,8 +97,13 @@ public class LoanRepaymentScheduleProcessingWrapper { Money cumulative = Money.zero(currency); for (final LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isFeeCharge()) { - cumulative = cumulative.plus(loanCharge.getAmountWaived(currency)); + if(loanCharge.isFeeCharge()){ + if(loanCharge.isInstalmentFee()){ + LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); + cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency)); + }else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)) { + cumulative = cumulative.plus(loanCharge.getAmountWaived(currency)); + } } } @@ -110,8 +116,13 @@ public class LoanRepaymentScheduleProcessingWrapper { Money cumulative = Money.zero(currency); for (final LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isFeeCharge()) { + if(loanCharge.isFeeCharge()){ + if(loanCharge.isInstalmentFee()){ + LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); + cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency)); + }else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)) { cumulative = cumulative.plus(loanCharge.getAmountWrittenOff(currency)); + } } } @@ -141,7 +152,8 @@ public class LoanRepaymentScheduleProcessingWrapper { } else { cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments))); } - } else if (loanCharge.getChargeCalculation().isPercentageBased()) { + } else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && + loanCharge.getChargeCalculation().isPercentageBased()) { BigDecimal amount = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { amount = amount.add(period.getLoan().getSummary().getTotalPrincipalDisbursed()).add(period.getLoan().getSummary().getTotalInterestCharged()); @@ -167,8 +179,13 @@ public class LoanRepaymentScheduleProcessingWrapper { Money cumulative = Money.zero(currency); for (final LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) { - cumulative = cumulative.plus(loanCharge.getAmountWaived(currency)); + if (loanCharge.isPenaltyCharge()) { + if(loanCharge.isInstalmentFee()){ + LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); + cumulative = cumulative.plus(loanChargePerInstallment.getAmountWaived(currency)); + }else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)) { + cumulative = cumulative.plus(loanCharge.getAmountWaived(currency)); + } } } @@ -181,8 +198,13 @@ public class LoanRepaymentScheduleProcessingWrapper { Money cumulative = Money.zero(currency); for (final LoanCharge loanCharge : loanCharges) { - if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) && loanCharge.isPenaltyCharge()) { + if (loanCharge.isPenaltyCharge()) { + if(loanCharge.isInstalmentFee()){ + LoanInstallmentCharge loanChargePerInstallment = loanCharge.getInstallmentLoanCharge(periodEnd); + cumulative = cumulative.plus(loanChargePerInstallment.getAmountWrittenOff(currency)); + }else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd)) { cumulative = cumulative.plus(loanCharge.getAmountWrittenOff(currency)); + } } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 788a66b88..0e7c1bd80 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -16,6 +16,7 @@ import org.mifosplatform.organisation.monetary.domain.Money; import org.mifosplatform.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanChargePaidBy; +import org.mifosplatform.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.mifosplatform.portfolio.loanaccount.domain.LoanTransaction; @@ -68,12 +69,24 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen final List transactionstoBeProcessed = new ArrayList(); for (final LoanTransaction loanTransaction : transactionsPostDisbursement) { if (loanTransaction.isChargePayment()) { + final List chargePaymentInstallments = new ArrayList(); final Set chargePaidBies = loanTransaction.getLoanChargesPaid(); final Set transferCharges = new HashSet(); for (final LoanChargePaidBy chargePaidBy : chargePaidBies) { - transferCharges.add(chargePaidBy.getLoanCharge()); + LoanCharge loanCharge = chargePaidBy.getLoanCharge(); + transferCharges.add(loanCharge); + if(loanCharge.isInstalmentFee()){ + LoanRepaymentScheduleInstallment installment = null; + if(loanCharge.isFeeCharge()){ + installment = loanCharge.fetchRepaymentInstallment(loanTransaction.getFeeChargesPortion(currency)); + }else{ + installment = loanCharge.fetchRepaymentInstallment(loanTransaction.getPenaltyChargesPortion(currency)); + } + if(installment !=null){ + chargePaymentInstallments.add(installment); + } + } } - final List chargePaymentInstallments = new ArrayList(); LocalDate startDate = disbursementDate; for (final LoanRepaymentScheduleInstallment installment : installments) { for (final LoanCharge loanCharge : transferCharges) { @@ -172,16 +185,20 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen final Set loanFees = extractFeeCharges(charges); final Set loanPenalties = extractPenaltyCharges(charges); + Integer installmentNumber = null; + if(loanTransaction.isChargePayment() && installments.size() == 1){ + installmentNumber = installments.get(0).getInstallmentNumber(); + } if (loanTransaction.isNotWaiver()) { final Money feeCharges = loanTransaction.getFeeChargesPortion(currency); if (feeCharges.isGreaterThanZero()) { - updateFeeChargesPaidAmountBy(loanTransaction, feeCharges, loanFees); + updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees,installmentNumber); } final Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency); if (penaltyCharges.isGreaterThanZero()) { - updatePenaltyChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties); + updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties,installmentNumber); } } @@ -211,65 +228,46 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen return penaltyCharges; } - private void updateFeeChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set charges) { + private void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set charges,final Integer installmentNumber) { Money amountRemaining = feeCharges; - for (final LoanCharge loanCharge : charges) { - if (!loanCharge.isDueAtDisbursement()) { - if (loanCharge.isFeeCharge() && loanCharge.isNotFullyPaid() && amountRemaining.isGreaterThanZero()) { - final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges); - final Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining); - if (!amountPaidTowardsCharge.isZero()) { - if (!loanTransaction.isChargePayment()) { - final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, loanCharge, - amountPaidTowardsCharge.getAmount()); - loanTransaction.getLoanChargesPaid().add(loanChargePaidBy); - } - amountRemaining = amountRemaining.minus(amountPaidTowardsCharge); - } - } + while(amountRemaining.isGreaterThanZero()) { + final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges); + Money feeAmount = feeCharges.zero(); + if(loanTransaction.isChargePayment()){ + feeAmount = feeCharges; } - } - } - - private void updatePenaltyChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, - final Set charges) { - - Money amountRemaining = feeCharges; - for (final LoanCharge loanCharge : charges) { - if (!loanCharge.isDueAtDisbursement()) { - - if (loanCharge.isPenaltyCharge() && loanCharge.isNotFullyPaid() && amountRemaining.isGreaterThanZero()) { - final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges); - final Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining); - if (!amountPaidTowardsCharge.isZero()) { - if (!loanTransaction.isChargePayment()) { - final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, loanCharge, - amountPaidTowardsCharge.getAmount()); - loanTransaction.getLoanChargesPaid().add(loanChargePaidBy); - } - amountRemaining = amountRemaining.minus(amountPaidTowardsCharge); - } + final Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining,installmentNumber, feeAmount); + if (!amountPaidTowardsCharge.isZero()) { + if (!loanTransaction.isChargePayment()) { + final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, unpaidCharge, + amountPaidTowardsCharge.getAmount()); + loanTransaction.getLoanChargesPaid().add(loanChargePaidBy); } + amountRemaining = amountRemaining.minus(amountPaidTowardsCharge); } } + } private LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(final Set charges) { LoanCharge earliestUnpaidCharge = null; LoanCharge installemntCharge = null; + LoanInstallmentCharge chargePerInstallment = null; for (final LoanCharge loanCharge : charges) { if (loanCharge.isNotFullyPaid() && !loanCharge.isDueAtDisbursement()) { if(loanCharge.isInstalmentFee()){ - if(installemntCharge == null){ + LoanInstallmentCharge unpaidLoanChargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge(); + if(chargePerInstallment == null || chargePerInstallment.getRepaymentInstallment().getDueDate().isAfter(unpaidLoanChargePerInstallment.getRepaymentInstallment().getDueDate())){ installemntCharge = loanCharge; + chargePerInstallment = unpaidLoanChargePerInstallment; } }else if (earliestUnpaidCharge == null || loanCharge.getDueLocalDate().isBefore(earliestUnpaidCharge.getDueLocalDate())) { earliestUnpaidCharge = loanCharge; } } } - if(earliestUnpaidCharge == null || earliestUnpaidCharge.getDueLocalDate().isAfter(new LocalDate())){ + if(earliestUnpaidCharge == null || (chargePerInstallment!=null && earliestUnpaidCharge.getDueLocalDate().isAfter(chargePerInstallment.getRepaymentInstallment().getDueDate()))){ earliestUnpaidCharge = installemntCharge; } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index d582d2d81..15c3be22e 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -198,7 +198,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } else { cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments))); } - } else if (loanCharge.getChargeCalculation().isPercentageBased()) { + } else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) + && loanCharge.getChargeCalculation().isPercentageBased()) { BigDecimal amount = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { amount = amount.add(principalDisbursed.getAmount()).add(totalInterestChargedForFullLoanTerm.getAmount()); @@ -243,7 +244,8 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener } else { cumulative = cumulative.plus(loanCharge.amount().divide(BigDecimal.valueOf(numberOfRepayments))); } - } else if (loanCharge.getChargeCalculation().isPercentageBased()) { + } else if (loanCharge.isDueForCollectionFromAndUpToAndIncluding(periodStart, periodEnd) + && loanCharge.getChargeCalculation().isPercentageBased()) { BigDecimal amount = BigDecimal.ZERO; if (loanCharge.getChargeCalculation().isPercentageOfAmountAndInterest()) { amount = amount.add(principalDisbursed.getAmount()).add(totalInterestChargedForFullLoanTerm.getAmount()); diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java index e8c7d8ced..08083ec6b 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/serialization/LoanEventApiJsonValidator.java @@ -300,9 +300,9 @@ public final class LoanEventApiJsonValidator { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } Set transactionParameters = null; if (isChargeIdIncluded) { - transactionParameters = new HashSet(Arrays.asList("transactionDate", "locale", "dateFormat", "chargeId")); + transactionParameters = new HashSet(Arrays.asList("transactionDate", "locale", "dateFormat", "chargeId","dueDate","installmentNumber")); } else { - transactionParameters = new HashSet(Arrays.asList("transactionDate", "locale", "dateFormat")); + transactionParameters = new HashSet(Arrays.asList("transactionDate", "locale", "dateFormat","dueDate","installmentNumber")); } final Type typeOfMap = new TypeToken>() {}.getType(); @@ -319,6 +319,28 @@ public final class LoanEventApiJsonValidator { baseDataValidator.reset().parameter("chargeId").value(chargeId).notNull().integerGreaterThanZero(); } baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull(); + final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("installmentNumber", element); + baseDataValidator.reset().parameter("installmentNumber").value(installmentNumber).ignoreIfNull().integerGreaterThanZero(); + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateInstallmentChargeTransaction(final String json) { + + if (StringUtils.isBlank(json)) { return;} + Set transactionParameters = new HashSet(Arrays.asList("dueDate", "locale", "dateFormat", "installmentNumber")); + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); + + final List dataValidationErrors = new ArrayList(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors) + .resource("loan.charge.waive.transaction"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final Integer installmentNumber = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("installmentNumber", element); + baseDataValidator.reset().parameter("installmentNumber").value(installmentNumber).ignoreIfNull().integerGreaterThanZero(); throwExceptionIfValidationWarningsExist(dataValidationErrors); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 51f64bcec..dac0bc64a 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -5,8 +5,10 @@ */ package org.mifosplatform.portfolio.loanaccount.service; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -47,6 +49,8 @@ import org.mifosplatform.portfolio.group.exception.GroupNotActiveException; import org.mifosplatform.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine; import org.mifosplatform.portfolio.loanaccount.domain.Loan; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; +import org.mifosplatform.portfolio.loanaccount.domain.LoanChargeRepository; +import org.mifosplatform.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository; @@ -109,6 +113,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private final CalendarInstanceRepository calendarInstanceRepository; private final SavingsAccountAssembler savingsAccountAssembler; private final AccountAssociationsRepository accountAssociationsRepository; + private final LoanChargeRepository loanChargeRepository; @Autowired public LoanApplicationWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final FromJsonHelper fromJsonHelper, @@ -122,7 +127,8 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa final LoanSummaryWrapper loanSummaryWrapper, final GroupRepositoryWrapper groupRepository, final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, final CalendarRepository calendarRepository, final CalendarInstanceRepository calendarInstanceRepository, - final SavingsAccountAssembler savingsAccountAssembler, final AccountAssociationsRepository accountAssociationsRepository) { + final SavingsAccountAssembler savingsAccountAssembler, final AccountAssociationsRepository accountAssociationsRepository, + final LoanChargeRepository loanChargeRepository) { this.context = context; this.fromJsonHelper = fromJsonHelper; this.loanApplicationTransitionApiJsonValidator = loanApplicationTransitionApiJsonValidator; @@ -145,6 +151,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa this.calendarInstanceRepository = calendarInstanceRepository; this.savingsAccountAssembler = savingsAccountAssembler; this.accountAssociationsRepository = accountAssociationsRepository; + this.loanChargeRepository = loanChargeRepository; } private LoanLifecycleStateMachine defaultLoanLifecycleStateMachine() { @@ -333,7 +340,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa if (changes.containsKey("recalculateLoanSchedule")) { changes.remove("recalculateLoanSchedule"); - + final JsonElement parsedQuery = this.fromJsonHelper.parse(command.json()); final JsonQuery query = JsonQuery.from(command.json(), parsedQuery, this.fromJsonHelper); @@ -354,6 +361,14 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa final Note note = Note.loanNote(existingLoanApplication, submittedOnNote); this.noteRepository.save(note); } + + for (final LoanCharge loanCharge : existingLoanApplication.charges()) { + if(loanCharge.isInstalmentFee() && loanCharge.hasNoLoanInstallmentCharges()){ + Set chargePerInstallments = existingLoanApplication.generateInstallmentLoanCharges(loanCharge); + loanCharge.addLoanInstallmentCharges(chargePerInstallments); + this.loanChargeRepository.save(loanCharge); + } + } final Long calendarId = command.longValueOfParameterNamed("calendarId"); Calendar calendar = null; diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanChargeReadPlatformService.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanChargeReadPlatformService.java index 71fd6cb57..dfc9f8d10 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanChargeReadPlatformService.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanChargeReadPlatformService.java @@ -9,6 +9,7 @@ import java.util.Collection; import org.mifosplatform.portfolio.charge.data.ChargeData; import org.mifosplatform.portfolio.loanaccount.data.LoanChargeData; +import org.mifosplatform.portfolio.loanaccount.data.LoanInstallmentChargeData; public interface LoanChargeReadPlatformService { @@ -19,4 +20,6 @@ public interface LoanChargeReadPlatformService { LoanChargeData retrieveLoanChargeDetails(Long loanChargeId, Long loanId); Collection retrieveLoanChargesForFeePayment(Integer paymentMode, Integer loanStatus); + + Collection retrieveInstallmentLoanCharges(Long loanChargeId,boolean onlyPaymentPendingCharges); } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java index b82c6f860..38d29d941 100755 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanChargeReadPlatformServiceImpl.java @@ -21,6 +21,7 @@ import org.mifosplatform.portfolio.charge.data.ChargeData; import org.mifosplatform.portfolio.charge.service.ChargeDropdownReadPlatformService; import org.mifosplatform.portfolio.charge.service.ChargeEnumerations; import org.mifosplatform.portfolio.loanaccount.data.LoanChargeData; +import org.mifosplatform.portfolio.loanaccount.data.LoanInstallmentChargeData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; @@ -104,13 +105,13 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform final EnumOptionData paymentMode = ChargeEnumerations.chargePaymentMode(chargePaymentMode); final boolean paid = rs.getBoolean("paid"); final boolean waived = rs.getBoolean("waied"); - final BigDecimal minCap = rs.getBigDecimal("minCap"); - final BigDecimal maxCap = rs.getBigDecimal("maxCap"); + final BigDecimal minCap = rs.getBigDecimal("minCap"); + final BigDecimal maxCap = rs.getBigDecimal("maxCap"); final BigDecimal amountOrPercentage = rs.getBigDecimal("amountOrPercentage"); return new LoanChargeData(id, chargeId, name, currency, amount, amountPaid, amountWaived, amountWrittenOff, amountOutstanding, chargeTimeType, dueAsOfDate, chargeCalculationType, percentageOf, amountPercentageAppliedTo, penalty, paymentMode, - paid, waived, null,minCap,maxCap,amountOrPercentage); + paid, waived, null, minCap, maxCap, amountOrPercentage, null); } } @@ -127,7 +128,7 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform .retrieveSavingsCalculationTypes(); final List savingsChargeTimeTypeOptions = this.chargeDropdownReadPlatformService .retrieveSavingsCollectionTimeTypes(); - + return ChargeData.template(null, allowedChargeCalculationTypeOptions, null, allowedChargeTimeOptions, null, loansChargeCalculationTypeOptions, loansChargeTimeTypeOptions, savingsChargeCalculationTypeOptions, savingsChargeTimeTypeOptions); @@ -168,7 +169,10 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform public String schema() { return "lc.id as id, lc.due_for_collection_as_of_date as dueAsOfDate, " - + "lc.amount_outstanding_derived as amountOutstanding, " + "loan.id as loanId " + "from m_loan_charge lc " + + "lc.amount_outstanding_derived as amountOutstanding, " + + "lc.charge_time_enum as chargeTime, " + + "loan.id as loanId " + + "from m_loan_charge lc " + "join m_loan loan on loan.id = lc.loan_id "; } @@ -179,7 +183,46 @@ public class LoanChargeReadPlatformServiceImpl implements LoanChargeReadPlatform final LocalDate dueAsOfDate = JdbcSupport.getLocalDate(rs, "dueAsOfDate"); final Long loanId = rs.getLong("loanId"); final BigDecimal amountOutstanding = rs.getBigDecimal("amountOutstanding"); - return new LoanChargeData(id, dueAsOfDate, amountOutstanding, loanId); + final int chargeTime = rs.getInt("chargeTime"); + final EnumOptionData chargeTimeType = ChargeEnumerations.chargeTimeType(chargeTime); + + return new LoanChargeData(id, dueAsOfDate, amountOutstanding, chargeTimeType, loanId, null); + } + } + + @Override + public Collection retrieveInstallmentLoanCharges(Long loanChargeId,boolean onlyPaymentPendingCharges) { + final LoanInstallmentChargeMapper rm = new LoanInstallmentChargeMapper(); + String sql = "select " + rm.schema() + + "where lic.loan_charge_id= ? "; + if(onlyPaymentPendingCharges){ + sql = sql +"and lic.waived =0 and lic.is_paid_derived=0"; + } + return this.jdbcTemplate.query(sql, rm, new Object[] { loanChargeId }); + } + + private static final class LoanInstallmentChargeMapper implements RowMapper { + + public String schema() { + return " lsi.installment as installmentNumber, lsi.duedate as dueAsOfDate, " + + "lic.amount_outstanding_derived as amountOutstanding," + + "lic.amount as amount, " + + "lic.is_paid_derived as paid, " + + "lic.waived as waied " + + "from m_loan_installment_charge lic " + + "join m_loan_repayment_schedule lsi on lsi.id = lic.loan_schedule_id "; + } + + @Override + public LoanInstallmentChargeData mapRow(final ResultSet rs, @SuppressWarnings("unused") final int rowNum) throws SQLException { + final Integer installmentNumber = rs.getInt("installmentNumber"); + final LocalDate dueAsOfDate = JdbcSupport.getLocalDate(rs, "dueAsOfDate"); + final BigDecimal amountOutstanding = rs.getBigDecimal("amountOutstanding"); + final BigDecimal amount = rs.getBigDecimal("amount"); + final boolean paid = rs.getBoolean("paid"); + final boolean waived = rs.getBoolean("waied"); + + return new LoanInstallmentChargeData(installmentNumber, dueAsOfDate, amount, amountOutstanding, paid, waived); } } } diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index ebe68b3e5..d4a5f88e3 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -77,11 +77,13 @@ import org.mifosplatform.portfolio.group.domain.Group; import org.mifosplatform.portfolio.group.exception.GroupNotActiveException; import org.mifosplatform.portfolio.loanaccount.command.LoanUpdateCommand; import org.mifosplatform.portfolio.loanaccount.data.LoanChargeData; +import org.mifosplatform.portfolio.loanaccount.data.LoanInstallmentChargeData; import org.mifosplatform.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.mifosplatform.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine; import org.mifosplatform.portfolio.loanaccount.domain.Loan; import org.mifosplatform.portfolio.loanaccount.domain.LoanAccountDomainService; import org.mifosplatform.portfolio.loanaccount.domain.LoanCharge; +import org.mifosplatform.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.mifosplatform.portfolio.loanaccount.domain.LoanChargeRepository; import org.mifosplatform.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.mifosplatform.portfolio.loanaccount.domain.LoanRepository; @@ -264,6 +266,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf for (final LoanCharge loanCharge : loanCharges) { if (loanCharge.isDueAtDisbursement() && loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { disBuLoanCharges.put(loanCharge.getId(), loanCharge.amount()); + }else if(loanCharge.isInstalmentFee() && loanCharge.hasNoLoanInstallmentCharges()){ + Set chargePerInstallments = loan.generateInstallmentLoanCharges(loanCharge); + loanCharge.addLoanInstallmentCharges(chargePerInstallments); + this.loanChargeRepository.save(loanCharge); } } final Locale locale = command.extractLocale(); @@ -272,7 +278,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final PortfolioAccountData savingAccountData = this.accountAssociationsReadPlatformService.retriveLoanAssociation(loanId); final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, entrySet.getValue(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, savingAccountData.accountId(), loanId, "Loan Charge Payment", - locale, fmt, null, null, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue(), entrySet.getKey()); + locale, fmt, null, null, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue(), entrySet.getKey(), null); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); } @@ -382,6 +388,26 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); } + final Set loanCharges = loan.charges(); + final Map disBuLoanCharges = new HashMap(); + for (final LoanCharge loanCharge : loanCharges) { + if (loanCharge.isDueAtDisbursement() && loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { + disBuLoanCharges.put(loanCharge.getId(), loanCharge.amount()); + }else if(loanCharge.isInstalmentFee() && loanCharge.hasNoLoanInstallmentCharges()){ + Set chargePerInstallments = loan.generateInstallmentLoanCharges(loanCharge); + loanCharge.addLoanInstallmentCharges(chargePerInstallments); + this.loanChargeRepository.save(loanCharge); + } + } + final Locale locale = command.extractLocale(); + final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); + for (final Map.Entry entrySet : disBuLoanCharges.entrySet()) { + final PortfolioAccountData savingAccountData = this.accountAssociationsReadPlatformService.retriveLoanAssociation(loan.getId()); + final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(actualDisbursementDate, entrySet.getValue(), + PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, savingAccountData.accountId(), loan.getId(), "Loan Charge Payment", + locale, fmt, null, null, LoanTransactionType.REPAYMENT_AT_DISBURSEMENT.getValue(), entrySet.getKey(), null); + this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + } } return changes; } @@ -914,6 +940,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = this.loanAssembler.assembleFrom(loanId); checkClientOrGroupActive(loan); + this.loanEventApiJsonValidator.validateInstallmentChargeTransaction(command.json()); final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); // Charges may be waived only when the loan associated with them are @@ -926,9 +953,28 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf throw new LoanChargeCannotBeWaivedException(LOAN_CHARGE_CANNOT_BE_WAIVED_REASON.ALREADY_WAIVED, loanCharge.getId()); } else if (loanCharge.isPaid()) { throw new LoanChargeCannotBeWaivedException(LOAN_CHARGE_CANNOT_BE_WAIVED_REASON.ALREADY_PAID, loanCharge.getId()); - } else if (loanCharge.isInstalmentFee()){ - throw new LoanChargeCannotBeWaivedException(LOAN_CHARGE_CANNOT_BE_WAIVED_REASON.WAIVE_NOT_ALLOWED_FOR_CHARGE, - loanCharge.getId()); + } + Integer loanInstallmentNumber = null; + if (loanCharge.isInstalmentFee()){ + LoanInstallmentCharge chargePerInstallment = null; + if(!StringUtils.isBlank(command.json())){ + final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); + final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); + if(dueDate !=null){ + chargePerInstallment = loanCharge.getInstallmentLoanCharge(dueDate); + }else if(installmentNumber!=null){ + chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber); + } + } + if(chargePerInstallment == null){ + chargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge(); + } + if(chargePerInstallment.isWaived()){ + throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_WAIVED, loanCharge.getId()); + }else if(chargePerInstallment.isPaid()){ + throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_PAID, loanCharge.getId()); + } + loanInstallmentNumber = chargePerInstallment.getRepaymentInstallment().getInstallmentNumber(); } final Map changes = new LinkedHashMap(3); @@ -937,7 +983,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final List existingReversedTransactionIds = new ArrayList(); final LoanTransaction waiveTransaction = loan.waiveLoanCharge(loanCharge, defaultLoanLifecycleStateMachine(), changes, - existingTransactionIds, existingReversedTransactionIds); + existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber); this.loanTransactionRepository.save(waiveTransaction); this.loanRepository.save(loan); @@ -1006,9 +1052,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_WAIVED, loanCharge.getId()); } else if (loanCharge.isPaid()) { throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_PAID, loanCharge.getId()); - }else if (loanCharge.isInstalmentFee()){ - throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.CHARGE_NOT_PAYABLE, - loanCharge.getId()); } if (!loanCharge.getChargePaymentMode().isPaymentModeAccountTransfer()) { throw new LoanChargeCannotBePayedException( @@ -1018,15 +1061,37 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Locale locale = command.extractLocale(); final DateTimeFormatter fmt = DateTimeFormat.forPattern(command.dateFormat()).withLocale(locale); - + Integer loanInstallmentNumber = null; + BigDecimal amount = loanCharge.amountOutstanding(); + if (loanCharge.isInstalmentFee()){ + LoanInstallmentCharge chargePerInstallment = null; + final LocalDate dueDate = command.localDateValueOfParameterNamed("dueDate"); + final Integer installmentNumber = command.integerValueOfParameterNamed("installmentNumber"); + if(dueDate !=null){ + chargePerInstallment = loanCharge.getInstallmentLoanCharge(dueDate); + }else if(installmentNumber!=null){ + chargePerInstallment = loanCharge.getInstallmentLoanCharge(installmentNumber); + } + if(chargePerInstallment == null){ + chargePerInstallment = loanCharge.getUnpaidInstallmentLoanCharge(); + } + if(chargePerInstallment.isWaived()){ + throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_WAIVED, loanCharge.getId()); + }else if(chargePerInstallment.isPaid()){ + throw new LoanChargeCannotBePayedException(LOAN_CHARGE_CANNOT_BE_PAYED_REASON.ALREADY_PAID, loanCharge.getId()); + } + loanInstallmentNumber = chargePerInstallment.getRepaymentInstallment().getInstallmentNumber(); + amount= chargePerInstallment.getAmountOutstanding(); + } + final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService.retriveLoanAssociation(loanId); if (portfolioAccountData == null) { final String errorMessage = "Charge with id:" + loanChargeId + " requires linked savings account for payment"; throw new LinkedAccountRequiredException("loanCharge.pay", errorMessage, loanChargeId); } - final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, loanCharge.amountOutstanding(), + final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(transactionDate, amount, PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, portfolioAccountData.accountId(), loanId, "Loan Charge Payment", - locale, fmt, null, null, LoanTransactionType.CHARGE_PAYMENT.getValue(), loanChargeId); + locale, fmt, null, null, LoanTransactionType.CHARGE_PAYMENT.getValue(), loanChargeId, loanInstallmentNumber); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); return new CommandProcessingResultBuilder() // @@ -1047,29 +1112,53 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final StringBuilder sb = new StringBuilder(); if (chargeDatas != null) { for (final LoanChargeData chargeData : chargeDatas) { - if (chargeData.getDueDate() != null && !chargeData.getDueDate().isAfter(new LocalDate())) { + if(chargeData.isInstallmentFee()){ + Collection chargePerInstallments = this.LoanChargeReadPlatformService.retrieveInstallmentLoanCharges(chargeData.getId(), true); + PortfolioAccountData portfolioAccountData = null; + for(LoanInstallmentChargeData installmentChargeData : chargePerInstallments){ + if(!installmentChargeData.getDueDate().isAfter(new LocalDate())){ + if(portfolioAccountData == null){ + portfolioAccountData = this.accountAssociationsReadPlatformService + .retriveLoanAssociation(chargeData.getLoanId()); + } + final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(new LocalDate(), + installmentChargeData.getAmountOutstanding(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, + portfolioAccountData.accountId(), chargeData.getLoanId(), "Loan Charge Payment", null, null, null, null, + LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId(), installmentChargeData.getInstallmentNumber()); + transferFeeCharge(sb, accountTransferDTO); + } + } + }else if (chargeData.getDueDate() != null && !chargeData.getDueDate().isAfter(new LocalDate())) { final PortfolioAccountData portfolioAccountData = this.accountAssociationsReadPlatformService .retriveLoanAssociation(chargeData.getLoanId()); final AccountTransferDTO accountTransferDTO = new AccountTransferDTO(new LocalDate(), chargeData.getAmountOutstanding(), PortfolioAccountType.SAVINGS, PortfolioAccountType.LOAN, portfolioAccountData.accountId(), chargeData.getLoanId(), "Loan Charge Payment", null, null, null, null, - LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId()); - try { - this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); - } catch (final PlatformApiDataValidationException e) { - sb.append("Validation exception while paying charge ").append(chargeData.getId()).append(" for loan id:") - .append(chargeData.getLoanId()).append("--------"); - } catch (final InsufficientAccountBalanceException e) { - sb.append("InsufficientAccountBalance Exception while paying charge ").append(chargeData.getId()) - .append("for loan id:").append(chargeData.getLoanId()).append("--------"); - - } + LoanTransactionType.CHARGE_PAYMENT.getValue(), chargeData.getId(), null); + transferFeeCharge(sb, accountTransferDTO); } } } if (sb.length() > 0) { throw new JobExecutionException(sb.toString()); } } + /** + * @param sb + * @param accountTransferDTO + */ + private void transferFeeCharge(final StringBuilder sb, final AccountTransferDTO accountTransferDTO) { + try { + this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); + } catch (final PlatformApiDataValidationException e) { + sb.append("Validation exception while paying charge ").append(accountTransferDTO.getChargeId()).append(" for loan id:") + .append(accountTransferDTO.getToAccountId()).append("--------"); + } catch (final InsufficientAccountBalanceException e) { + sb.append("InsufficientAccountBalance Exception while paying charge ").append(accountTransferDTO.getChargeId()) + .append("for loan id:").append(accountTransferDTO.getToAccountId()).append("--------"); + + } + } + private LoanCharge retrieveLoanChargeBy(final Long loanId, final Long loanChargeId) { final LoanCharge loanCharge = this.loanChargeRepository.findOne(loanChargeId); if (loanCharge == null) { throw new LoanChargeNotFoundException(loanChargeId); }