tracking transactions that repay loan charges

This commit is contained in:
Vishwas Babu AJ
2013-06-05 14:24:39 +05:30
parent 86a9badb56
commit c62ee07d45
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()) {
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<LoanCharge> charges) {
private void updateFeeChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set<LoanCharge> 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<LoanCharge> charges) {
private void updatePenaltyChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges,
final Set<LoanCharge> 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);
}
}
}
}
@@ -101,7 +101,7 @@ public class Loan extends AbstractPersistable<Long> {
@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<Long> {
@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<LoanCharge> loanCharges, final Set<LoanCollateral> collateral) {
final LoanStatus status = null;
@@ -304,8 +304,8 @@ public class Loan extends AbstractPersistable<Long> {
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<LoanCharge> loanCharges, final Set<LoanCollateral> collateral) {
if (StringUtils.isBlank(accountNo)) {
@@ -677,10 +677,10 @@ public class Loan extends AbstractPersistable<Long> {
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<Long> {
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<Long> {
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<Long> {
actualChanges.put(collateralParamName, listOfLoanCollateralData(possiblyModifedLoanCollateralItems));
}
}
return actualChanges;
}
@@ -1081,16 +1085,18 @@ public class Loan extends AbstractPersistable<Long> {
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<Long> {
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<Long> {
}
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);
}
@@ -449,6 +449,11 @@ public class LoanCharge extends AbstractPersistable<Long> {
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<Long> {
this.paid = determineIfFullyPaid();
return incrementBy.minus(amountPaidOnThisCharge);
return amountPaidOnThisCharge;
}
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.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<Long> {
@Column(name = "is_reversed", nullable = false)
private boolean reversed;
@LazyCollection(LazyCollectionOption.FALSE)
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loanTransaction", orphanRemoval = true)
private Set<LoanChargePaidBy> loanChargesPaid = new HashSet<LoanChargePaidBy>();
protected LoanTransaction() {
this.loan = null;
this.dateOf = null;
@@ -250,6 +260,10 @@ public final class LoanTransaction extends AbstractPersistable<Long> {
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<Long> {
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);
this.loanTransactionRepository.save(newRepaymentTransaction);
/***
* TODO Vishwas Batch save is giving me a
* HibernateOptimisticLockingFailureException, looping and saving for