Merge pull request #334 from vishwasbabu/mifosx-413

tracking transactions that repay loan charges
This commit is contained in:
Vishwas Babu A J
2013-06-05 01:59:05 -07:00
8 changed files with 183 additions and 43 deletions
@@ -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;
@@ -133,12 +133,12 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
if (loanTransaction.isNotWaiver()) { if (loanTransaction.isNotWaiver()) {
Money feeCharges = loanTransaction.getFeeChargesPortion(currency); Money feeCharges = loanTransaction.getFeeChargesPortion(currency);
if (feeCharges.isGreaterThanZero()) { if (feeCharges.isGreaterThanZero()) {
updateFeeChargesPaidAmountBy(feeCharges, charges); updateFeeChargesPaidAmountBy(loanTransaction, feeCharges, charges);
} }
Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency); Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency);
if (penaltyCharges.isGreaterThanZero()) { 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<LoanCharge> charges) { private void updateFeeChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> charges) {
Money amountRemaining = feeCharges; Money amountRemaining = feeCharges;
for (LoanCharge loanCharge : charges) { for (LoanCharge loanCharge : charges) {
if (!loanCharge.isDueAtDisbursement()) { if (!loanCharge.isDueAtDisbursement()) {
if (loanCharge.isFeeCharge() && loanCharge.isNotFullyPaid() && amountRemaining.isGreaterThanZero()) { if (loanCharge.isFeeCharge() && loanCharge.isNotFullyPaid() && amountRemaining.isGreaterThanZero()) {
final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges); 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<LoanCharge> charges) { private void updatePenaltyChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges,
final Set<LoanCharge> charges) {
Money amountRemaining = feeCharges; Money amountRemaining = feeCharges;
for (LoanCharge loanCharge : charges) { for (LoanCharge loanCharge : charges) {
@@ -168,7 +175,13 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
if (loanCharge.isPenaltyCharge() && amountRemaining.isGreaterThanZero()) { if (loanCharge.isPenaltyCharge() && amountRemaining.isGreaterThanZero()) {
final LoanCharge unpaidCharge = findEarliestUnpaidChargeFromUnOrderedSet(charges); 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);
}
} }
} }
} }
@@ -264,8 +264,8 @@ public class Loan extends AbstractPersistable<Long> {
@Transient @Transient
private LoanSummaryWrapper loanSummaryWrapper; private LoanSummaryWrapper loanSummaryWrapper;
public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType, final LoanProduct loanProduct, public static Loan newIndividualLoanApplication(final String accountNo, final Client client, final Integer loanType,
final Fund fund, final Staff officer, final CodeValue loanPurpose, final LoanProduct loanProduct, final Fund fund, final Staff officer, final CodeValue loanPurpose,
final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanSchedule loanSchedule, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanSchedule loanSchedule,
final Set<LoanCharge> loanCharges, final Set<LoanCollateral> collateral) { final Set<LoanCharge> loanCharges, final Set<LoanCollateral> collateral) {
final LoanStatus status = null; final LoanStatus status = null;
@@ -304,8 +304,8 @@ public class Loan extends AbstractPersistable<Long> {
this.client = null; this.client = null;
} }
private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund, final Staff loanOfficer, private Loan(final String accountNo, final Client client, final Group group, final Integer loanType, final Fund fund,
final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy, final Staff loanOfficer, final CodeValue loanPurpose, final LoanTransactionProcessingStrategy transactionProcessingStrategy,
final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus, final LoanProduct loanProduct, final LoanProductRelatedDetail loanRepaymentScheduleDetail, final LoanStatus loanStatus,
final Set<LoanCharge> loanCharges, final Set<LoanCollateral> collateral) { final Set<LoanCharge> loanCharges, final Set<LoanCollateral> collateral) {
if (StringUtils.isBlank(accountNo)) { if (StringUtils.isBlank(accountNo)) {
@@ -677,7 +677,7 @@ public class Loan extends AbstractPersistable<Long> {
return this.client; return this.client;
} }
public LoanProduct loanProduct(){ public LoanProduct loanProduct() {
return this.loanProduct; return this.loanProduct;
} }
@@ -822,8 +822,9 @@ public class Loan extends AbstractPersistable<Long> {
this.externalId = StringUtils.defaultIfEmpty(newValue, null); this.externalId = StringUtils.defaultIfEmpty(newValue, null);
} }
//add clientId, groupId and loanType changes to actual changes // 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 // FIXME: AA - We may require separate api command to move loan from one
// client to another
final String clientIdParamName = "clientId"; final String clientIdParamName = "clientId";
final Long clientId = this.client == null ? null : this.client.getId(); final Long clientId = this.client == null ? null : this.client.getId();
if (command.isChangeInLongParameterNamed(clientIdParamName, clientId)) { if (command.isChangeInLongParameterNamed(clientIdParamName, clientId)) {
@@ -831,7 +832,8 @@ public class Loan extends AbstractPersistable<Long> {
actualChanges.put(clientIdParamName, newValue); 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 String groupIdParamName = "groupId";
final Long groupId = this.group == null ? null : this.group.getId(); final Long groupId = this.group == null ? null : this.group.getId();
if (command.isChangeInLongParameterNamed(groupIdParamName, groupId)) { if (command.isChangeInLongParameterNamed(groupIdParamName, groupId)) {
@@ -943,16 +945,18 @@ public class Loan extends AbstractPersistable<Long> {
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, getSubmittedOnDate()); throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, getSubmittedOnDate());
} }
if (!(client == null)){ if (!(client == null)) {
if (getSubmittedOnDate().isBefore(client.getActivationLocalDate())) { if (getSubmittedOnDate().isBefore(client.getActivationLocalDate())) {
final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; 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()); throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage,
} getSubmittedOnDate());
}else if (!(group == null)){ }
if (getSubmittedOnDate().isBefore(group.getActivationLocalDate())) { } else if (!(group == null)) {
final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; if (getSubmittedOnDate().isBefore(group.getActivationLocalDate())) {
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, getSubmittedOnDate()); 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())) { if (getSubmittedOnDate().isAfter(getExpectedDisbursedOnLocalDate())) {
@@ -1081,16 +1085,18 @@ public class Loan extends AbstractPersistable<Long> {
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOn); throw new InvalidLoanStateTransitionException("submittal", "cannot.be.a.future.date", errorMessage, submittedOn);
} }
if (!(client == null)){ if (!(client == null)) {
if (submittedOn.isBefore(client.getActivationLocalDate())) { if (submittedOn.isBefore(client.getActivationLocalDate())) {
final String errorMessage = "The date on which a loan is submitted cannot be earlier than client's activation date."; 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); throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.client.activation.date", errorMessage,
} submittedOn);
}else if (!(group == null)){ }
if (submittedOn.isBefore(group.getActivationLocalDate())) { } else if (!(group == null)) {
final String errorMessage = "The date on which a loan is submitted cannot be earlier than groups's activation date."; if (submittedOn.isBefore(group.getActivationLocalDate())) {
throw new InvalidLoanStateTransitionException("submittal", "cannot.be.before.group.activation.date", errorMessage, submittedOn); 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())) { if (submittedOn.isAfter(getExpectedDisbursedOnLocalDate())) {
@@ -1405,6 +1411,9 @@ public class Loan extends AbstractPersistable<Long> {
for (LoanCharge charge : setOfLoanCharges()) { for (LoanCharge charge : setOfLoanCharges()) {
if (charge.isDueAtDisbursement()) { if (charge.isDueAtDisbursement()) {
charge.markAsFullyPaid(); charge.markAsFullyPaid();
// Add "Loan Charge Paid By" details to this transaction
LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(disbursementTransaction, charge, charge.amount());
disbursementTransaction.getLoanChargesPaid().add(loanChargePaidBy);
} else { } else {
handleChargeAppliedTransaction(charge, disbursedOn); handleChargeAppliedTransaction(charge, disbursedOn);
} }
@@ -1419,14 +1428,15 @@ public class Loan extends AbstractPersistable<Long> {
} }
if (disbursedOn.isAfter(new LocalDate())) { 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); throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.a.future.date", errorMessage, disbursedOn);
} }
LocalDate firstRepaymentDueDate = this.repaymentScheduleInstallments.get(0).getDueDate(); LocalDate firstRepaymentDueDate = this.repaymentScheduleInstallments.get(0).getDueDate();
if (disbursedOn.isAfter(firstRepaymentDueDate)) { 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: " final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber
+ firstRepaymentDueDate.toString(); + " is disbursed cannot be after the first expected repayment date: " + firstRepaymentDueDate.toString();
throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.first.repayment.due.date", errorMessage, throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.first.repayment.due.date", errorMessage,
this.accountNumber, disbursedOn, firstRepaymentDueDate); this.accountNumber, disbursedOn, firstRepaymentDueDate);
} }
@@ -449,6 +449,11 @@ public class LoanCharge extends AbstractPersistable<Long> {
return Money.of(currency, this.amountWrittenOff); 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) { public Money updatePaidAmountBy(final Money incrementBy) {
Money amountPaidToDate = Money.of(incrementBy.getCurrency(), this.amountPaid); Money amountPaidToDate = Money.of(incrementBy.getCurrency(), this.amountPaid);
@@ -471,7 +476,7 @@ public class LoanCharge extends AbstractPersistable<Long> {
this.paid = determineIfFullyPaid(); this.paid = determineIfFullyPaid();
return incrementBy.minus(amountPaidOnThisCharge); return amountPaidOnThisCharge;
} }
public String name() { public String name() {
@@ -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<Long> {
@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;
}
}
@@ -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<LoanChargePaidBy, Long>, JpaSpecificationExecutor<LoanCharge> {
// no added behaviour
}
@@ -7,17 +7,23 @@ package org.mifosplatform.portfolio.loanaccount.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.Date; import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Set;
import javax.persistence.CascadeType;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.JoinColumn; import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne; import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table; import javax.persistence.Table;
import javax.persistence.Temporal; import javax.persistence.Temporal;
import javax.persistence.TemporalType; import javax.persistence.TemporalType;
import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;
import org.joda.time.LocalDate; import org.joda.time.LocalDate;
import org.mifosplatform.organisation.monetary.data.CurrencyData; import org.mifosplatform.organisation.monetary.data.CurrencyData;
import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency; import org.mifosplatform.organisation.monetary.domain.MonetaryCurrency;
@@ -70,6 +76,10 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
@Column(name = "is_reversed", nullable = false) @Column(name = "is_reversed", nullable = false)
private boolean reversed; private boolean reversed;
@LazyCollection(LazyCollectionOption.FALSE)
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loanTransaction", orphanRemoval = true)
private Set<LoanChargePaidBy> loanChargesPaid = new HashSet<LoanChargePaidBy>();
protected LoanTransaction() { protected LoanTransaction() {
this.loan = null; this.loan = null;
this.dateOf = null; this.dateOf = null;
@@ -250,6 +260,10 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
return !isReversed(); return !isReversed();
} }
public boolean isAnyTypeOfRepayment() {
return isRepayment() || isRepaymentAtDisbursement() || isRecoveryRepayment();
}
public boolean isRepayment() { public boolean isRepayment() {
return LoanTransactionType.REPAYMENT.equals(getTypeOf()) && isNotReversed(); return LoanTransactionType.REPAYMENT.equals(getTypeOf()) && isNotReversed();
} }
@@ -367,4 +381,12 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
return this.loan; return this.loan;
} }
public Set<LoanChargePaidBy> getLoanChargesPaid() {
return this.loanChargesPaid;
}
public void setLoanChargesPaid(Set<LoanChargePaidBy> loanChargesPaid) {
this.loanChargesPaid = loanChargesPaid;
}
} }
@@ -337,6 +337,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds); defaultLoanLifecycleStateMachine(), existingTransactionIds, existingReversedTransactionIds);
this.loanTransactionRepository.save(newRepaymentTransaction); this.loanTransactionRepository.save(newRepaymentTransaction);
/*** /***
* TODO Vishwas Batch save is giving me a * TODO Vishwas Batch save is giving me a
* HibernateOptimisticLockingFailureException, looping and saving for * HibernateOptimisticLockingFailureException, looping and saving for