diff --git a/mifosng-db/patches/23-charges_updates.sql b/mifosng-db/patches/23-charges_updates.sql new file mode 100755 index 000000000..acea34e28 --- /dev/null +++ b/mifosng-db/patches/23-charges_updates.sql @@ -0,0 +1,2 @@ +ALTER TABLE `m_loan_charge` + ADD COLUMN `waived` TINYINT(1) NOT NULL DEFAULT '0' AFTER `is_paid_derived`; \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeDeletedException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeDeletedException.java new file mode 100755 index 000000000..1b8b8ed5a --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeDeletedException.java @@ -0,0 +1,38 @@ +/** + * 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.charge.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class LoanChargeCannotBeDeletedException extends AbstractPlatformDomainRuleException { + + /*** enum of reasons of why Loan Charge cannot be waived **/ + public static enum LOAN_CHARGE_CANNOT_BE_DELETED_REASON { + ALREADY_PAID, ALREADY_WAIVED, LOAN_APPROVED; + + public String errorMessage() { + if (name().toString().equalsIgnoreCase("ALREADY_PAID")) { + return "This loan charge has been partially/completely paid"; + } else if (name().toString().equalsIgnoreCase("ALREADY_WAIVED")) { + return "This loan charge has already been waived"; + } else if (name().toString().equalsIgnoreCase("LOAN_APPROVED")) { return "This charge cannot be deleted as the loan it is associated with has already been approved"; } + return name().toString(); + } + + public String errorCode() { + if (name().toString().equalsIgnoreCase("ALREADY_PAID")) { + return "error.msg.loan.charge.already.paid"; + } else if (name().toString().equalsIgnoreCase("ALREADY_WAIVED")) { + return "error.msg.loan.charge.already.waived"; + } else if (name().toString().equalsIgnoreCase("LOAN_APPROVED")) { return "error.msg.loan.charge.associated.loan.already.approved"; } + return name().toString(); + } + } + + public LoanChargeCannotBeDeletedException(final LOAN_CHARGE_CANNOT_BE_DELETED_REASON reason, final Long loanChargeId) { + super(reason.errorCode(), reason.errorMessage(), loanChargeId); + } +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeWaivedException.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeWaivedException.java new file mode 100755 index 000000000..24edfdd3f --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/charge/exception/LoanChargeCannotBeWaivedException.java @@ -0,0 +1,38 @@ +/** + * 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.charge.exception; + +import org.mifosplatform.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class LoanChargeCannotBeWaivedException extends AbstractPlatformDomainRuleException { + + /*** enum of reasons of why Loan Charge cannot be waived **/ + public static enum LOAN_CHARGE_CANNOT_BE_WAIVED_REASON { + ALREADY_PAID, ALREADY_WAIVED, LOAN_INACTIVE; + + public String errorMessage() { + if (name().toString().equalsIgnoreCase("ALREADY_PAID")) { + return "This loan charge has been completely paid"; + } else if (name().toString().equalsIgnoreCase("ALREADY_WAIVED")) { + return "This loan charge has already been waived"; + } else if (name().toString().equalsIgnoreCase("LOAN_INACTIVE")) { return "This loan charge can be waived as the loan associated with it is currently inactive"; } + return name().toString(); + } + + public String errorCode() { + if (name().toString().equalsIgnoreCase("ALREADY_PAID")) { + return "error.msg.loan.charge.already.paid"; + } else if (name().toString().equalsIgnoreCase("ALREADY_WAIVED")) { + return "error.msg.loan.charge.already.waived"; + } else if (name().toString().equalsIgnoreCase("LOAN_INACTIVE")) { return "error.msg.loan.charge.associated.loan.inactive"; } + return name().toString(); + } + } + + public LoanChargeCannotBeWaivedException(final LOAN_CHARGE_CANNOT_BE_WAIVED_REASON reason, final Long loanChargeId) { + super(reason.errorCode(), reason.errorMessage(), loanChargeId); + } +} 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 b0755a080..6fd0dd243 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 @@ -430,7 +430,7 @@ public class Loan extends AbstractPersistable { } private void validateLoanChargeIsNotWaived(final LoanCharge loanCharge) { - if (loanCharge.isWaivedOrPartiallyWaived(loanCurrency())) { + if (loanCharge.isWaived()) { final String defaultUserMessage = "This loan charge cannot be removed as the charge as already been waived."; throw new LoanChargeCannotBeAddedException("loanCharge", "loanCharge.is.waived", defaultUserMessage, getId(), loanCharge.name()); } @@ -1793,7 +1793,7 @@ public class Loan extends AbstractPersistable { return !isSubmittedAndPendingApproval(); } - private LoanStatus status() { + public LoanStatus status() { return LoanStatus.fromInt(this.loanStatus); } 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 a59fd91ee..b383ca3be 100644 --- 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 @@ -82,6 +82,9 @@ public class LoanCharge extends AbstractPersistable { @Column(name = "is_paid_derived", nullable = false) private boolean paid = false; + @Column(name = "waived", nullable = false) + private boolean waived = false; + public static LoanCharge createNewFromJson(final Loan loan, final Charge chargeDefinition, final JsonCommand command) { final BigDecimal amount = command.bigDecimalValueOfParameterNamed("amount"); @@ -101,8 +104,7 @@ public class LoanCharge extends AbstractPersistable { // ChargeCalculationType.fromInt(chargeCalculationType); // } - return new LoanCharge(loan, chargeDefinition, loan.getPrincpal().getAmount(), amount, chargeTime, chargeCalculation, - dueDate); + return new LoanCharge(loan, chargeDefinition, loan.getPrincpal().getAmount(), amount, chargeTime, chargeCalculation, dueDate); } /* @@ -132,8 +134,8 @@ public class LoanCharge extends AbstractPersistable { if (dueDate == null) { final String defaultUserMessage = "Loan charge is missing due date."; - throw new LoanChargeWithoutMandatoryFieldException("loanCharge", "dueDate", defaultUserMessage, - chargeDefinition.getId(), chargeDefinition.getName()); + throw new LoanChargeWithoutMandatoryFieldException("loanCharge", "dueDate", defaultUserMessage, chargeDefinition.getId(), + chargeDefinition.getName()); } this.dueDate = dueDate.toDate(); @@ -218,6 +220,7 @@ public class LoanCharge extends AbstractPersistable { this.amountWrittenOff = BigDecimal.ZERO; this.amountOutstanding = calculateAmountOutstanding(currency); this.paid = false; + this.waived = false; } public void resetPaidAmount(final MonetaryCurrency currency) { @@ -227,9 +230,10 @@ public class LoanCharge extends AbstractPersistable { } public Money waive(final MonetaryCurrency currency) { - this.amountWaived = this.amount; - this.amountOutstanding = calculateAmountOutstanding(currency); - this.paid = determineIfFullyPaid(); + this.amountWaived = this.amountOutstanding; + this.amountOutstanding = BigDecimal.ZERO; + this.paid = false; + this.waived = true; return getAmountWaived(currency); } @@ -452,16 +456,16 @@ public class LoanCharge extends AbstractPersistable { return this.paid; } + public boolean isWaived() { + return this.waived; + } + public boolean isPaidOrPartiallyPaid(final MonetaryCurrency currency) { final Money amountWaivedOrWrittenOff = getAmountWaived(currency).plus(getAmountWrittenOff(currency)); return Money.of(currency, this.amountPaid).plus(amountWaivedOrWrittenOff).isGreaterThanZero(); } - public boolean isWaivedOrPartiallyWaived(final MonetaryCurrency currency) { - return getAmountWaived(currency).isGreaterThanZero(); - } - private Money getAmount(final MonetaryCurrency currency) { return Money.of(currency, this.amount); } 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 aefc1c817..ec68ed406 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 @@ -24,6 +24,10 @@ import org.mifosplatform.organisation.monetary.domain.ApplicationCurrencyReposit import org.mifosplatform.organisation.staff.domain.Staff; import org.mifosplatform.portfolio.charge.domain.Charge; import org.mifosplatform.portfolio.charge.domain.ChargeRepositoryWrapper; +import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeDeletedException; +import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeDeletedException.LOAN_CHARGE_CANNOT_BE_DELETED_REASON; +import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeWaivedException; +import org.mifosplatform.portfolio.charge.exception.LoanChargeCannotBeWaivedException.LOAN_CHARGE_CANNOT_BE_WAIVED_REASON; import org.mifosplatform.portfolio.charge.exception.LoanChargeNotFoundException; import org.mifosplatform.portfolio.loanaccount.command.LoanUpdateCommand; import org.mifosplatform.portfolio.loanaccount.domain.DefaultLoanLifecycleStateMachine; @@ -143,7 +147,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf context.authenticatedUser(); final Loan loan = retrieveLoanBy(loanId); - final BigDecimal totalInterestCharged = loan.getSummary().getTotalInterestCharged(); + final BigDecimal totalInterestCharged = loan.getSummary().getTotalInterestCharged(); final List existingTransactionIds = new ArrayList(); final List existingReversedTransactionIds = new ArrayList(); @@ -164,7 +168,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf // accountingBridgedate, this would not work if interest // recalculation in introduced accountingBridgeData.put("calculatedInterest", totalInterestCharged); - + journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData); } @@ -310,7 +314,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Note note = Note.loanTransactionNote(loan, waiveTransaction, noteText); this.noteRepository.save(note); } - + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); return new CommandProcessingResultBuilder() // @@ -535,16 +539,28 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = retrieveLoanBy(loanId); final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); + // Charges may be waived only when the loan associated with them are + // active + if (!loan.status().isActive()) { throw new LoanChargeCannotBeWaivedException(LOAN_CHARGE_CANNOT_BE_WAIVED_REASON.LOAN_INACTIVE, + loanCharge.getId()); } + + // validate loan charge is not already paid or waived + if (loanCharge.isWaived()) { + 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()); } + final Map changes = new LinkedHashMap(3); - + final List existingTransactionIds = new ArrayList(); final List existingReversedTransactionIds = new ArrayList(); + final LoanTransaction waiveTransaction = loan.waiveLoanCharge(loanCharge, defaultLoanLifecycleStateMachine(), changes, existingTransactionIds, existingReversedTransactionIds); this.loanTransactionRepository.save(waiveTransaction); this.loanRepository.save(loan); - + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); return new CommandProcessingResultBuilder() // @@ -558,7 +574,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .build(); } - @Transactional @Override public CommandProcessingResult deleteLoanCharge(final Long loanId, final Long loanChargeId, final JsonCommand command) { @@ -568,6 +583,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf final Loan loan = retrieveLoanBy(loanId); final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId); + // validate loan charge is not partly paid or waived + if (loanCharge.isWaived()) { + throw new LoanChargeCannotBeDeletedException(LOAN_CHARGE_CANNOT_BE_DELETED_REASON.ALREADY_WAIVED, loanCharge.getId()); + } else if (loanCharge.isPaidOrPartiallyPaid(loan.getCurrency())) { throw new LoanChargeCannotBeDeletedException( + LOAN_CHARGE_CANNOT_BE_DELETED_REASON.ALREADY_PAID, loanCharge.getId()); } + loan.removeLoanCharge(loanCharge); this.loanRepository.save(loan); @@ -688,7 +709,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .withLoanId(loanId) // .build(); } - + /** * @param loan * @param existingTransactionIds