mifosx-58: loan charges at time of disbursement

This commit is contained in:
Keith Woodlock
2012-09-13 17:20:49 +01:00
parent 16da04f089
commit 384928670d
10 changed files with 169 additions and 87 deletions
@@ -0,0 +1,2 @@
ALTER TABLE `m_loan`
ADD COLUMN `total_charges_due_at_disbursement_derived` bigint(20) DEFAULT NULL AFTER `interest_rebate_amount`;
@@ -47,7 +47,7 @@ public class Charge extends AbstractAuditableCustom<AppUser, Long> {
//
}
public Charge(final String name, final BigDecimal amount, final String currencyCode,
private Charge(final String name, final BigDecimal amount, final String currencyCode,
final ChargeAppliesTo chargeAppliesTo, final ChargeTimeType chargeTime,
final ChargeCalculationType chargeCalculationType, final boolean active) {
this.name = name;
@@ -2,10 +2,10 @@ package org.mifosng.platform.charge.domain;
public enum ChargeCalculationType {
INVALID(0, "chargeCalculationType.invalid"),
FLAT(1, "chargeCalculationType.flat"),
PERCENT_OF_AMOUNT(2, "chargeCalculationType.percent.of.amount"),
PERCENT_OF_AMOUNT_AND_INTEREST(3, "chargeCalculationType.percent.of.amount.and.interest"),
INVALID(0, "chargeCalculationType.invalid"), //
FLAT(1, "chargeCalculationType.flat"), //
PERCENT_OF_AMOUNT(2, "chargeCalculationType.percent.of.amount"), //
PERCENT_OF_AMOUNT_AND_INTEREST(3, "chargeCalculationType.percent.of.amount.and.interest"), //
PERCENT_OF_INTEREST(4, "chargeCalculationType.percent.of.interest");
private final Integer value;
@@ -155,6 +155,10 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
@Column(name = "interest_rebate_amount", scale = 6, precision = 19)
private BigDecimal interestRebateOwed;
@SuppressWarnings("unused") // derived field used in GETs only at present
@Column(name = "total_charges_due_at_disbursement_derived", scale = 6, precision = 19)
private BigDecimal totalChargesDueAtDisbursement;
@Transient
private final InterestRebateCalculatorFactory interestRebateCalculatorFactory = new DailyEquivalentInterestRebateCalculatorFactory();
@@ -164,8 +168,8 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
public static Loan createNew(final Fund fund,final Staff loanOfficer, final LoanTransactionProcessingStrategy transactionProcessingStrategy,
final LoanProduct loanProduct, final Client client,
final LoanProductRelatedDetail loanRepaymentScheduleDetail) {
return new Loan(client, fund,loanOfficer, transactionProcessingStrategy, loanProduct, loanRepaymentScheduleDetail, null);
final LoanProductRelatedDetail loanRepaymentScheduleDetail, final Set<LoanCharge> loanCharges) {
return new Loan(client, fund,loanOfficer, transactionProcessingStrategy, loanProduct, loanRepaymentScheduleDetail, null, loanCharges);
}
protected Loan() {
@@ -175,9 +179,12 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.charges = null;
}
public Loan(final Client client, Fund fund, Staff loanOfficer, LoanTransactionProcessingStrategy transactionProcessingStrategy, final LoanProduct loanProduct,
private Loan(
final Client client, Fund fund, Staff loanOfficer,
final LoanTransactionProcessingStrategy transactionProcessingStrategy,
final LoanProduct loanProduct,
final LoanProductRelatedDetail loanRepaymentScheduleDetail,
final LoanStatus loanStatus) {
final LoanStatus loanStatus, final Set<LoanCharge> loanCharges) {
this.client = client;
this.fund = fund;
this.loanofficer = loanOfficer;
@@ -189,9 +196,37 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
} else {
this.loanStatus = null;
}
if (loanCharges != null && !loanCharges.isEmpty()) {
this.charges = associateChargesWithThisLoan(loanCharges);
this.totalChargesDueAtDisbursement = deriveSumTotalOfChargesDueAtDisbursement();
} else {
this.charges = null;
}
this.interestRebateOwed = BigDecimal.ZERO;
}
private BigDecimal deriveSumTotalOfChargesDueAtDisbursement() {
Money chargesDue = Money.of(getCurrency(), BigDecimal.ZERO);
for (LoanCharge charge : this.charges) {
if (charge.isDueAtDisbursement()) {
chargesDue = chargesDue.plus(charge.calculateMonetaryAmount());
}
}
return chargesDue.getAmount();
}
private Set<LoanCharge> associateChargesWithThisLoan(Set<LoanCharge> loanCharges) {
for (LoanCharge loanCharge : loanCharges) {
loanCharge.update(this);
}
return loanCharges;
}
public Client client() {
return this.client;
}
@@ -266,7 +301,9 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
if (command.isChargesChanged()) {
this.charges.clear();
this.charges.addAll(charges);
this.charges = associateChargesWithThisLoan(charges);
this.totalChargesDueAtDisbursement = deriveSumTotalOfChargesDueAtDisbursement();
}
this.loanRepaymentScheduleDetail.update(command.toLoanProductCommand());
@@ -1217,4 +1254,8 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
return this.loanRepaymentScheduleDetail.getPrincipal()
.getCurrencyCode();
}
public Money getPrincpal() {
return this.loanRepaymentScheduleDetail.getPrincipal();
}
}
@@ -1,18 +1,27 @@
package org.mifosng.platform.loan.domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.mifosng.platform.api.commands.LoanChargeCommand;
import org.mifosng.platform.charge.domain.Charge;
import javax.persistence.*;
import java.io.Serializable;
import java.math.BigDecimal;
import org.mifosng.platform.charge.domain.ChargeCalculationType;
import org.mifosng.platform.charge.domain.ChargeTimeType;
@Entity
@IdClass(LoanCharge.LoanChargePK.class)
@Table(name = "m_loan_charge")
public class LoanCharge {
@SuppressWarnings("unused")
@Id
private Loan loan;
@@ -20,50 +29,55 @@ public class LoanCharge {
@Id
private Charge charge;
@SuppressWarnings("unused")
@Column(name = "amount", scale = 6, precision = 19, nullable = false)
private BigDecimal amount;
@SuppressWarnings("unused")
@Column(name = "charge_time_enum", nullable = false)
private Integer chargeTime;
@SuppressWarnings("unused")
@Column(name = "charge_calculation_enum")
private Integer chargeCalculation;
public static LoanCharge createNew(final Charge chargeDefinition, final LoanChargeCommand command) {
return new LoanCharge(null, chargeDefinition, command);
}
public static LoanCharge createNew(final Charge chargeDefinition) {
return new LoanCharge(null, chargeDefinition);
}
protected LoanCharge() {
//
}
public LoanCharge(Loan loan, Charge charge, LoanChargeCommand command) {
private LoanCharge(final Loan loan, final Charge chargeDefinition, final LoanChargeCommand command) {
this.loan = loan;
this.charge = charge;
this.charge = chargeDefinition;
if (command.isAmountChanged()){
this.amount = command.getAmount();
} else {
this.amount = charge.getAmount();
this.amount = chargeDefinition.getAmount();
}
if (command.isChargeTimeTypeChanged()){
this.chargeTime = command.getChargeTimeType();
} else {
this.chargeTime = charge.getChargeTime();
this.chargeTime = chargeDefinition.getChargeTime();
}
if (command.isChargeCalculationTypeChanged()){
this.chargeCalculation = command.getChargeCalculationType();
} else {
this.chargeCalculation = charge.getChargeCalculation();
this.chargeCalculation = chargeDefinition.getChargeCalculation();
}
}
public LoanCharge(final Loan loan, final Charge charge) {
this(loan, charge, charge.getAmount(), charge.getChargeTime(), charge.getChargeCalculation());
private LoanCharge(final Loan loan, final Charge chargeDefinition) {
this(loan, chargeDefinition, chargeDefinition.getAmount(), chargeDefinition.getChargeTime(), chargeDefinition.getChargeCalculation());
}
public LoanCharge(final Loan loan, final Charge charge, final BigDecimal amount, final Integer chargeTime, final Integer chargeCalculation) {
private LoanCharge(final Loan loan, final Charge charge, final BigDecimal amount, final Integer chargeTime, final Integer chargeCalculation) {
this.loan = loan;
this.charge = charge;
this.amount = amount;
@@ -84,4 +98,34 @@ public class LoanCharge {
@JoinColumn(name = "charge_id", referencedColumnName = "id", nullable=false)
private Charge charge;
}
}
public void update(final Loan loan) {
this.loan = loan;
}
public boolean isDueAtDisbursement() {
return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.DISBURSEMENT);
}
public BigDecimal calculateMonetaryAmount() {
BigDecimal calculatedAmount = BigDecimal.ZERO;
if (ChargeCalculationType.fromInt(chargeCalculation).equals(ChargeCalculationType.FLAT)) {
calculatedAmount = this.amount;
} else if (ChargeCalculationType.fromInt(chargeCalculation).equals(ChargeCalculationType.PERCENT_OF_AMOUNT)) {
BigDecimal amountAsPercentageFactor = BigDecimal.ZERO;
if (isGreaterThanZero(this.amount)) {
amountAsPercentageFactor = this.amount.divide(BigDecimal.valueOf(Double.valueOf("100")));
}
calculatedAmount = this.loan.getPrincpal().multiplyRetainScale(amountAsPercentageFactor, RoundingMode.HALF_EVEN).getAmount();
}
return calculatedAmount;
}
private boolean isGreaterThanZero(final BigDecimal value) {
return value.compareTo(BigDecimal.ZERO) == 1;
}
}
@@ -52,7 +52,7 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
@Enumerated(EnumType.ORDINAL)
@Column(name = "transaction_type_enum", nullable = false)
private LoanTransactionType typeOf;
private Integer typeOf;
@OneToOne(optional=true, cascade={CascadeType.PERSIST})
@JoinColumn(name="contra_id")
@@ -65,32 +65,30 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
this.typeOf = null;
}
public static LoanTransaction disbursement(Money amount, LocalDate disbursementDate) {
public static LoanTransaction disbursement(final Money amount, final LocalDate disbursementDate) {
return new LoanTransaction(LoanTransactionType.DISBURSEMENT, amount.getAmount(), disbursementDate);
}
public static LoanTransaction repayment(Money amount, LocalDate paymentDate) {
public static LoanTransaction repayment(final Money amount, final LocalDate paymentDate) {
return new LoanTransaction(LoanTransactionType.REPAYMENT, amount.getAmount(), paymentDate);
}
public static LoanTransaction waiver(Money waived, LocalDate waiveDate) {
public static LoanTransaction waiver(final Money waived, final LocalDate waiveDate) {
return new LoanTransaction(LoanTransactionType.WAIVED, waived.getAmount(), waiveDate);
}
private static LoanTransaction contra(LoanTransaction originalTransaction) {
LoanTransaction contra = new LoanTransaction(LoanTransactionType.REVERSAL, originalTransaction.getAmount().negate(), new LocalDate(originalTransaction.getDateOf()));
contra.updateContra(originalTransaction);
return contra;
}
public void updateContra(LoanTransaction transaction) {
public void updateContra(final LoanTransaction transaction) {
this.contra = transaction;
}
private LoanTransaction(LoanTransactionType type, final BigDecimal amount, final LocalDate date) {
this.typeOf = type;
private LoanTransaction(final LoanTransactionType type, final BigDecimal amount, final LocalDate date) {
this.typeOf = type.getValue();
this.amount = amount;
this.dateOf = date.toDateMidnight().toDate();
}
@@ -113,11 +111,11 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
}
public LoanTransactionType getTypeOf() {
return typeOf;
return LoanTransactionType.fromInt(this.typeOf);
}
public boolean isRepayment() {
return LoanTransactionType.REPAYMENT.equals(typeOf) && isNotContra();
return LoanTransactionType.REPAYMENT.equals(getTypeOf()) && isNotContra();
}
public boolean isNotRepayment() {
@@ -129,11 +127,11 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
}
public boolean isDisbursement() {
return LoanTransactionType.DISBURSEMENT.equals(typeOf);
return LoanTransactionType.DISBURSEMENT.equals(getTypeOf());
}
public boolean isWaiver() {
return LoanTransactionType.WAIVED.equals(typeOf) && isNotContra();
return LoanTransactionType.WAIVED.equals(getTypeOf()) && isNotContra();
}
public boolean isNotWaiver() {
@@ -1,7 +1,12 @@
package org.mifosng.platform.loan.domain;
public enum LoanTransactionType {
INVALID(0, "loanTransactionType.invalid"), DISBURSEMENT(1, "loanTransactionType.disbursement"), REPAYMENT(2, "loanTransactionType.repayment"), REVERSAL(3, "loanTransactionType.reversal"), WAIVED(4, "loanTransactionType.waiver");
INVALID(0, "loanTransactionType.invalid"), //
DISBURSEMENT(1, "loanTransactionType.disbursement"), //
REPAYMENT(2, "loanTransactionType.repayment"), //
REVERSAL(3, "loanTransactionType.reversal"), //
WAIVED(4, "loanTransactionType.waiver");
private final Integer value;
private final String code;
@@ -6,8 +6,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.mifosng.platform.api.commands.LoanChargeCommand;
import org.mifosng.platform.api.commands.LoanApplicationCommand;
import org.mifosng.platform.api.commands.LoanChargeCommand;
import org.mifosng.platform.api.data.LoanSchedule;
import org.mifosng.platform.api.data.MoneyData;
import org.mifosng.platform.api.data.ScheduledLoanInstallment;
@@ -20,7 +20,6 @@ import org.mifosng.platform.exceptions.ChargeIsNotActiveException;
import org.mifosng.platform.exceptions.ChargeNotFoundException;
import org.mifosng.platform.exceptions.ClientNotFoundException;
import org.mifosng.platform.exceptions.FundNotFoundException;
import org.mifosng.platform.exceptions.InvalidCurrencyException;
import org.mifosng.platform.exceptions.LoanProductNotFoundException;
import org.mifosng.platform.exceptions.LoanTransactionProcessingStrategyNotFoundException;
import org.mifosng.platform.exceptions.StaffNotFoundException;
@@ -94,22 +93,23 @@ public class LoanAssembler {
LoanProductRelatedDetail loanRepaymentScheduleDetail = assembleLoanProductRelatedDetailFrom(command, currency);
LoanSchedule loanSchedule = command.getLoanSchedule();
List<ScheduledLoanInstallment> loanRepaymentSchedule = loanSchedule.getScheduledLoanInstallments();
// associating fund with loan product at creation is optional for now.
Fund fund = findFundByIdIfProvided(command.getFundId());
LoanTransactionProcessingStrategy loanTransactionProcessingStrategy = findStrategyByIdIfProvided(command.getTransactionProcessingStrategyId());
//optionally associate a loan officer to the loan
Staff loanOfficer= findLoanOfficerByIdIfProvided(command.getLoanOfficerId());
Loan loan = Loan.createNew(fund,loanOfficer, loanTransactionProcessingStrategy, loanProduct, client, loanRepaymentScheduleDetail);
// optionally, see if charges are associated with loan on creation (through loan product or by being directly added)
final Set<LoanCharge> loanCharges = assembleSetOfLoanCharges(command.getCharges(), loanProduct.getCharges());
Loan loan = Loan.createNew(fund,loanOfficer, loanTransactionProcessingStrategy, loanProduct, client, loanRepaymentScheduleDetail, loanCharges);
loan.setExternalId(command.getExternalId());
final Set<LoanCharge> charges = this.assembleSetOfCharges(command, loan, loanProduct);
loan.setCharges(charges);
// TODO - user service to calculate loan schedule at this point
LoanSchedule loanSchedule = command.getLoanSchedule();
List<ScheduledLoanInstallment> loanRepaymentSchedule = loanSchedule.getScheduledLoanInstallments();
for (ScheduledLoanInstallment scheduledLoanInstallment : loanRepaymentSchedule) {
MoneyData readPrincipalDue = scheduledLoanInstallment.getPrincipalDue();
@@ -130,6 +130,33 @@ public class LoanAssembler {
return loan;
}
public Set<LoanCharge> assembleSetOfLoanCharges(LoanChargeCommand[] chargesPassedAtCreation, Set<Charge> chargesInheritedFromProduct) {
Set<LoanCharge> loanCharges = new HashSet<LoanCharge>();
if (!ObjectUtils.isEmpty(chargesPassedAtCreation)) {
for (LoanChargeCommand loanChargeCommand : chargesPassedAtCreation) {
Long chargeDefinitionId = loanChargeCommand.getId();
Charge chargeDefinition = this.chargeRepository.findOne(chargeDefinitionId);
if (chargeDefinition == null || chargeDefinition.isDeleted()) {
throw new ChargeNotFoundException(chargeDefinitionId);
}
if (!chargeDefinition.isActive()) {
throw new ChargeIsNotActiveException(chargeDefinitionId);
}
loanCharges.add(LoanCharge.createNew(chargeDefinition, loanChargeCommand));
}
} else if (chargesPassedAtCreation == null) {
for (Charge productCharge : chargesInheritedFromProduct) {
loanCharges.add(LoanCharge.createNew(productCharge));
}
}
return loanCharges;
}
public LoanProductRelatedDetail assembleLoanProductRelatedDetailFrom(final LoanApplicationCommand command, MonetaryCurrency currency) {
final BigDecimal defaultNominalInterestRatePerPeriod = command.getInterestRatePerPeriod();
final PeriodFrequencyType interestPeriodFrequencyType = PeriodFrequencyType.fromInt(command.getInterestRateFrequencyType());
@@ -193,39 +220,4 @@ public class LoanAssembler {
}
return strategy;
}
public Set<LoanCharge> assembleSetOfCharges(final LoanApplicationCommand command, final Loan loan,
final LoanProduct product) {
Set<LoanCharge> charges = new HashSet<LoanCharge>();
LoanChargeCommand[] loanChargesArray = command.getCharges();
String loanCurrencyCode = loan.getCurrencyCode();
if (loanCurrencyCode == null){
loanCurrencyCode = product.getCurrency().getCode();
}
if (!ObjectUtils.isEmpty(loanChargesArray)) {
for (LoanChargeCommand loanChargeCommand : loanChargesArray) {
Long id = loanChargeCommand.getId();
Charge charge = this.chargeRepository.findOne(id);
if (charge == null || charge.isDeleted()) {
throw new ChargeNotFoundException(id);
}
if (!charge.isActive()){
throw new ChargeIsNotActiveException(id);
}
if (!loanCurrencyCode.equals(charge.getCurrencyCode())){
String errorMessage = "Charge and Loan must have the same currency.";
throw new InvalidCurrencyException("charge", "attach.to.loan", errorMessage);
}
charges.add(new LoanCharge(loan, charge, loanChargeCommand));
}
} else if (loanChargesArray == null) {
for (Charge productCharge : product.getCharges()){
charges.add(new LoanCharge(loan, productCharge));
}
}
return charges;
}
}
@@ -145,7 +145,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
Fund fund = this.loanAssembler.findFundByIdIfProvided(command.getFundId());
Staff loanOfficer = this.loanAssembler.findLoanOfficerByIdIfProvided(command.getLoanOfficerId());
LoanTransactionProcessingStrategy strategy = this.loanAssembler.findStrategyByIdIfProvided(command.getTransactionProcessingStrategyId());
Set<LoanCharge> charges = this.loanAssembler.assembleSetOfCharges(command, loan, loanProduct) ;
Set<LoanCharge> charges = this.loanAssembler.assembleSetOfLoanCharges(command.getCharges(), loanProduct.getCharges()) ;
LoanSchedule loanSchedule = this.calculationPlatformService.calculateLoanSchedule(command.toCalculateLoanScheduleCommand());
loan.modifyLoanApplication(command, client, loanProduct, fund, strategy, loanSchedule, charges, loanOfficer);