diff --git a/mifosng-db/patches/patch-0001-heavensfamily-data-patch.sql b/mifosng-db/patches/patch-0002-heavensfamily-data-patch.sql similarity index 100% rename from mifosng-db/patches/patch-0001-heavensfamily-data-patch.sql rename to mifosng-db/patches/patch-0002-heavensfamily-data-patch.sql diff --git a/mifosng-db/patches/patch-0003-add-total-charges-due-at-disbursement-derived-column.sql b/mifosng-db/patches/patch-0003-add-total-charges-due-at-disbursement-derived-column.sql new file mode 100644 index 000000000..c0c03a1a6 --- /dev/null +++ b/mifosng-db/patches/patch-0003-add-total-charges-due-at-disbursement-derived-column.sql @@ -0,0 +1,2 @@ +ALTER TABLE `m_loan` +ADD COLUMN `total_charges_due_at_disbursement_derived` bigint(20) DEFAULT NULL AFTER `interest_rebate_amount`; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/charge/domain/Charge.java b/mifosng-provider/src/main/java/org/mifosng/platform/charge/domain/Charge.java index bfe88aa97..4717fba62 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/charge/domain/Charge.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/charge/domain/Charge.java @@ -47,7 +47,7 @@ public class Charge extends AbstractAuditableCustom { // } - 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; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/charge/domain/ChargeCalculationType.java b/mifosng-provider/src/main/java/org/mifosng/platform/charge/domain/ChargeCalculationType.java index ff901ad36..6cc5d4cb7 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/charge/domain/ChargeCalculationType.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/charge/domain/ChargeCalculationType.java @@ -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; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java index 0557fd44e..61f501ebd 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/Loan.java @@ -155,6 +155,10 @@ public class Loan extends AbstractAuditableCustom { @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 { 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 loanCharges) { + return new Loan(client, fund,loanOfficer, transactionProcessingStrategy, loanProduct, loanRepaymentScheduleDetail, null, loanCharges); } protected Loan() { @@ -175,9 +179,12 @@ public class Loan extends AbstractAuditableCustom { 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 loanCharges) { this.client = client; this.fund = fund; this.loanofficer = loanOfficer; @@ -189,9 +196,37 @@ public class Loan extends AbstractAuditableCustom { } 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 associateChargesWithThisLoan(Set 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 { 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 { return this.loanRepaymentScheduleDetail.getPrincipal() .getCurrencyCode(); } + + public Money getPrincpal() { + return this.loanRepaymentScheduleDetail.getPrincipal(); + } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanCharge.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanCharge.java index ef9ddcc54..27ad043a5 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanCharge.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanCharge.java @@ -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; + } +} \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java index c3469dd24..2d6768d43 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransaction.java @@ -52,7 +52,7 @@ public class LoanTransaction extends AbstractAuditableCustom { @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 { 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 { } 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 { } 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() { diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionType.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionType.java index 3f69ebd36..301564858 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionType.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/domain/LoanTransactionType.java @@ -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; diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanAssembler.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanAssembler.java index a5f4afecd..87c0b4758 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanAssembler.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanAssembler.java @@ -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 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 loanCharges = assembleSetOfLoanCharges(command.getCharges(), loanProduct.getCharges()); + + Loan loan = Loan.createNew(fund,loanOfficer, loanTransactionProcessingStrategy, loanProduct, client, loanRepaymentScheduleDetail, loanCharges); loan.setExternalId(command.getExternalId()); - final Set charges = this.assembleSetOfCharges(command, loan, loanProduct); - loan.setCharges(charges); - + // TODO - user service to calculate loan schedule at this point + LoanSchedule loanSchedule = command.getLoanSchedule(); + List loanRepaymentSchedule = loanSchedule.getScheduledLoanInstallments(); + for (ScheduledLoanInstallment scheduledLoanInstallment : loanRepaymentSchedule) { MoneyData readPrincipalDue = scheduledLoanInstallment.getPrincipalDue(); @@ -130,6 +130,33 @@ public class LoanAssembler { return loan; } + public Set assembleSetOfLoanCharges(LoanChargeCommand[] chargesPassedAtCreation, Set chargesInheritedFromProduct) { + Set loanCharges = new HashSet(); + + 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 assembleSetOfCharges(final LoanApplicationCommand command, final Loan loan, - final LoanProduct product) { - - Set charges = new HashSet(); - 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; - } } \ No newline at end of file diff --git a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 04130fb4e..46896cbdf 100644 --- a/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/mifosng-provider/src/main/java/org/mifosng/platform/loan/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -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 charges = this.loanAssembler.assembleSetOfCharges(command, loan, loanProduct) ; + Set 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);