diff --git a/mifosng-db/migrations/core_db/V49__track-loan-charge-payment-transactions.sql b/mifosng-db/migrations/core_db/V49__track-loan-charge-payment-transactions.sql new file mode 100755 index 000000000..206e0f909 --- /dev/null +++ b/mifosng-db/migrations/core_db/V49__track-loan-charge-payment-transactions.sql @@ -0,0 +1,14 @@ +CREATE TABLE `m_loan_charge_paid_by` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `loan_transaction_id` BIGINT(20) NOT NULL, + `loan_charge_id` BIGINT(20) NOT NULL, + `amount` DECIMAL(19,6) NOT NULL, + PRIMARY KEY (`id`), + INDEX `FK__m_loan_transaction` (`loan_transaction_id`), + INDEX `FK__m_loan_charge` (`loan_charge_id`), + CONSTRAINT `FK__m_loan_charge` FOREIGN KEY (`loan_charge_id`) REFERENCES `m_loan_charge` (`id`), + CONSTRAINT `FK__m_loan_transaction` FOREIGN KEY (`loan_transaction_id`) REFERENCES `m_loan_transaction` (`id`) +) +COLLATE='utf8_general_ci' +ENGINE=InnoDB; + diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java index 7412242bc..a9d4fea52 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -133,12 +133,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen if (loanTransaction.isNotWaiver()) { Money feeCharges = loanTransaction.getFeeChargesPortion(currency); if (feeCharges.isGreaterThanZero()) { - updateFeeChargesPaidAmountBy(feeCharges, charges); + updateFeeChargesPaidAmountBy(loanTransaction, feeCharges, charges); } Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency); if (penaltyCharges.isGreaterThanZero()) { - updatePenaltyChargesPaidAmountBy(penaltyCharges, charges); + updatePenaltyChargesPaidAmountBy(loanTransaction, penaltyCharges, charges); } } @@ -147,20 +147,27 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen } } - private void updateFeeChargesPaidAmountBy(final Money feeCharges, final Set charges) { + private void updateFeeChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set charges) { Money amountRemaining = feeCharges; for (LoanCharge loanCharge : charges) { if (!loanCharge.isDueAtDisbursement()) { if (loanCharge.isFeeCharge() && loanCharge.isNotFullyPaid() && amountRemaining.isGreaterThanZero()) { final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges); - amountRemaining = unpaidCharge.updatePaidAmountBy(amountRemaining); + Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining); + if (!amountPaidTowardsCharge.isZero()) { + LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, loanCharge, + amountPaidTowardsCharge.getAmount()); + loanTransaction.getLoanChargesPaid().add(loanChargePaidBy); + amountRemaining = amountRemaining.minus(amountPaidTowardsCharge); + } } } } } - private void updatePenaltyChargesPaidAmountBy(final Money feeCharges, final Set charges) { + private void updatePenaltyChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, + final Set charges) { Money amountRemaining = feeCharges; for (LoanCharge loanCharge : charges) { @@ -168,7 +175,13 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen if (loanCharge.isPenaltyCharge() && amountRemaining.isGreaterThanZero()) { final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges); - amountRemaining = unpaidCharge.updatePaidAmountBy(amountRemaining); + Money amountPaidTowardsCharge = unpaidCharge.updatePaidAmountBy(amountRemaining); + if (!amountPaidTowardsCharge.isZero()) { + LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(loanTransaction, loanCharge, + amountPaidTowardsCharge.getAmount()); + loanTransaction.getLoanChargesPaid().add(loanChargePaidBy); + amountRemaining = amountRemaining.minus(amountPaidTowardsCharge); + } } } } 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 19a2ec498..b6df12500 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 @@ -101,7 +101,7 @@ public class Loan extends AbstractPersistable { @SuppressWarnings("unused") @Column(name = "loan_type_enum", nullable = false) private Integer loanType; - + @ManyToOne @JoinColumn(name = "product_id", nullable = false) private LoanProduct loanProduct; @@ -264,8 +264,8 @@ public class Loan extends AbstractPersistable { @Transient private LoanSummaryWrapper loanSummaryWrapper; - public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType, final LoanProduct loanProduct, - final Fund fund, final Staff officer, final CodeValue loanPurpose, + public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType, + final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanSchedule loanSchedule, final Set loanCharges, final Set collateral) { final LoanStatus status = null; @@ -304,8 +304,8 @@ public class Loan extends AbstractPersistable { this.client = null; } - private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund, final Staff loanOfficer, - final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, + private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund, + final Staff loanOfficer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus, final Set loanCharges, final Set collateral) { if (StringUtils.isBlank(accountNo)) { @@ -677,10 +677,10 @@ public class Loan extends AbstractPersistable { return this.client; } - public LoanProduct loanProduct(){ + public LoanProduct loanProduct() { return this.loanProduct; } - + public LoanProductRelatedDetail repaymentScheduleDetail() { return this.loanRepaymentScheduleDetail; } @@ -822,16 +822,18 @@ public class Loan extends AbstractPersistable { this.externalId = StringUtils.defaultIfEmpty(newValue, null); } - //add clientId, groupId and loanType changes to actual changes - //FIXME: AA - We may require separate api command to move loan from one client to another + // add clientId, groupId and loanType changes to actual changes + // FIXME: AA - We may require separate api command to move loan from one + // client to another final String clientIdParamName = "clientId"; final Long clientId = this.client == null ? null : this.client.getId(); if (command.isChangeInLongParameterNamed(clientIdParamName, clientId)) { final Long newValue = command.longValueOfParameterNamed(clientIdParamName); actualChanges.put(clientIdParamName, newValue); } - - //FIXME: AA - We may require separate api command to move loan from one group to another + + // FIXME: AA - We may require separate api command to move loan from one + // group to another final String groupIdParamName = "groupId"; final Long groupId = this.group == null ? null : this.group.getId(); if (command.isChangeInLongParameterNamed(groupIdParamName, groupId)) { @@ -942,17 +944,19 @@ public class Loan extends AbstractPersistable { final String errorMessage = "The date on which a loan is submitted cannot be in the future."; throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, getSubmittedOnDate()); } - - if (!(client == null)){ - if (getSubmittedOnDate().isBefore(client.getActivationLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, getSubmittedOnDate()); - } - }else if (!(group == null)){ - if (getSubmittedOnDate().isBefore(group.getActivationLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, getSubmittedOnDate()); - } + + if (!(client == null)) { + if (getSubmittedOnDate().isBefore(client.getActivationLocalDate())) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, + getSubmittedOnDate()); + } + } else if (!(group == null)) { + if (getSubmittedOnDate().isBefore(group.getActivationLocalDate())) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, + getSubmittedOnDate()); + } } if (getSubmittedOnDate().isAfter(getExpectedDisbursedOnLocalDate())) { @@ -986,7 +990,7 @@ public class Loan extends AbstractPersistable { actualChanges.put(collateralParamName, listOfLoanCollateralData(possiblyModifedLoanCollateralItems)); } } - + return actualChanges; } @@ -1081,16 +1085,18 @@ public class Loan extends AbstractPersistable { throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOn); } - if (!(client == null)){ - if (submittedOn.isBefore(client.getActivationLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, submittedOn); - } - }else if (!(group == null)){ - if (submittedOn.isBefore(group.getActivationLocalDate())) { - final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; - throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, submittedOn); - } + if (!(client == null)) { + if (submittedOn.isBefore(client.getActivationLocalDate())) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage, + submittedOn); + } + } else if (!(group == null)) { + if (submittedOn.isBefore(group.getActivationLocalDate())) { + final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; + throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, + submittedOn); + } } if (submittedOn.isAfter(getExpectedDisbursedOnLocalDate())) { @@ -1405,6 +1411,9 @@ public class Loan extends AbstractPersistable { for (LoanCharge charge : setOfLoanCharges()) { if (charge.isDueAtDisbursement()) { charge.markAsFullyPaid(); + // Add "Loan Charge Paid By" details to this transaction + LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(disbursementTransaction, charge, charge.amount()); + disbursementTransaction.getLoanChargesPaid().add(loanChargePaidBy); } else { handleChargeAppliedTransaction(charge, disbursedOn); } @@ -1419,14 +1428,15 @@ public class Loan extends AbstractPersistable { } if (disbursedOn.isAfter(new LocalDate())) { - final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber + " is disbursed cannot be in the future."; + final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber + + " is disbursed cannot be in the future."; throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.a.future.date", errorMessage, disbursedOn); } LocalDate firstRepaymentDueDate = this.repaymentScheduleInstallments.get(0).getDueDate(); if (disbursedOn.isAfter(firstRepaymentDueDate)) { - final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber + " is disbursed cannot be after the first expected repayment date: " - + firstRepaymentDueDate.toString(); + final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber + + " is disbursed cannot be after the first expected repayment date: " + firstRepaymentDueDate.toString(); throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.first.repayment.due.date", errorMessage, this.accountNumber, disbursedOn, firstRepaymentDueDate); } 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 1ebc1d960..a1a8ff9e7 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 @@ -449,6 +449,11 @@ public class LoanCharge extends AbstractPersistable { return Money.of(currency, this.amountWrittenOff); } + /** + * @param incrementBy + * Amount used to pay off this charge + * @return Actual amount paid on this charge + */ public Money updatePaidAmountBy(final Money incrementBy) { Money amountPaidToDate = Money.of(incrementBy.getCurrency(), this.amountPaid); @@ -471,7 +476,7 @@ public class LoanCharge extends AbstractPersistable { this.paid = determineIfFullyPaid(); - return incrementBy.minus(amountPaidOnThisCharge); + return amountPaidOnThisCharge; } public String name() { diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidBy.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidBy.java new file mode 100755 index 000000000..c2bd409b8 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidBy.java @@ -0,0 +1,62 @@ +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.springframework.data.jpa.domain.AbstractPersistable; + +@Entity +@Table(name = "m_loan_charge_paid_by") +public class LoanChargePaidBy extends AbstractPersistable { + + @ManyToOne + @JoinColumn(name = "loan_transaction_id", nullable = false) + private LoanTransaction loanTransaction; + + @ManyToOne + @JoinColumn(name = "loan_charge_id", nullable = false) + private LoanCharge loanCharge; + + @Column(name = "amount", scale = 6, precision = 19, nullable = false) + private BigDecimal amount; + + protected LoanChargePaidBy() { + + } + + public LoanChargePaidBy(LoanTransaction loanTransaction, LoanCharge loanCharge, BigDecimal amount) { + this.loanTransaction = loanTransaction; + this.loanCharge = loanCharge; + this.amount = amount; + } + + public LoanTransaction getLoanTransaction() { + return this.loanTransaction; + } + + public void setLoanTransaction(LoanTransaction loanTransaction) { + this.loanTransaction = loanTransaction; + } + + public LoanCharge getLoanCharge() { + return this.loanCharge; + } + + public void setLoanCharge(LoanCharge loanCharge) { + this.loanCharge = loanCharge; + } + + public BigDecimal getAmount() { + return this.amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidByRepository.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidByRepository.java new file mode 100755 index 000000000..5a84a0d49 --- /dev/null +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanChargePaidByRepository.java @@ -0,0 +1,13 @@ +/** + * 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 org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface LoanChargePaidByRepository extends JpaRepository, JpaSpecificationExecutor { + // no added behaviour +} diff --git a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java index 362a8229f..3e6a856a9 100644 --- a/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosplatform/portfolio/loanaccount/domain/LoanTransaction.java @@ -7,17 +7,23 @@ package org.mifosplatform.portfolio.loanaccount.domain; import java.math.BigDecimal; 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.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; import org.joda.time.LocalDate; import org.mifosplatform.organisation.monetary.data.CurrencyData; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; @@ -70,6 +76,10 @@ public final class LoanTransaction extends AbstractPersistable { @Column(name = "is_reversed", nullable = false) private boolean reversed; + @LazyCollection(LazyCollectionOption.FALSE) + @OneToMany(cascade = CascadeType.ALL, mappedBy = "loanTransaction", orphanRemoval = true) + private Set loanChargesPaid = new HashSet(); + protected LoanTransaction() { this.loan = null; this.dateOf = null; @@ -250,6 +260,10 @@ public final class LoanTransaction extends AbstractPersistable { return !isReversed(); } + public boolean isAnyTypeOfRepayment() { + return isRepayment() || isRepaymentAtDisbursement() || isRecoveryRepayment(); + } + public boolean isRepayment() { return LoanTransactionType.REPAYMENT.equals(getTypeOf()) && isNotReversed(); } @@ -367,4 +381,12 @@ public final class LoanTransaction extends AbstractPersistable { return this.loan; } + public Set getLoanChargesPaid() { + return this.loanChargesPaid; + } + + public void setLoanChargesPaid(Set loanChargesPaid) { + this.loanChargesPaid = loanChargesPaid; + } + } \ No newline at end of file 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 b6c285350..0102458e8 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 @@ -337,6 +337,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds); this.loanTransactionRepository.save(newRepaymentTransaction); + /*** * TODO Vishwas Batch save is giving me a * HibernateOptimisticLockingFailureException, looping and saving for