mifosx-58: cater for modification of loan charges during loan modify process. ensure existing loan charges are used and updated rather than removed and new ones added each time.

This commit is contained in:
Keith Woodlock
2012-09-28 16:53:00 +01:00
parent 3860d2af56
commit 4fc43ced64
10 changed files with 143 additions and 119 deletions
@@ -18,7 +18,6 @@ public class LoanChargeData {
private final EnumOptionData chargeTimeType;
@SuppressWarnings("unused")
private final EnumOptionData chargeCalculationType;
@SuppressWarnings("unused")
private final BigDecimal percentage;
@SuppressWarnings("unused")
private final BigDecimal amountPercentageAppliedTo;
@@ -30,6 +29,8 @@ public class LoanChargeData {
private final BigDecimal amountPaid;
@SuppressWarnings("unused")
private final BigDecimal amountOutstanding;
@SuppressWarnings("unused")
private final BigDecimal amountOrPercentage;
@SuppressWarnings("unused")
private final Collection<ChargeData> chargeOptions;
@@ -75,6 +76,12 @@ public class LoanChargeData {
this.percentage = percentage;
this.amountPercentageAppliedTo = amountPercentageAppliedTo;
if (chargeCalculationType != null && chargeCalculationType.getId().intValue() > 1) {
this.amountOrPercentage = this.percentage;
} else {
this.amountOrPercentage = amount;
}
this.chargeOptions = null;
}
@@ -98,6 +105,13 @@ public class LoanChargeData {
this.chargeCalculationType = chargeCalculationType;
this.percentage = null;
this.amountPercentageAppliedTo = null;
if (chargeCalculationType != null && chargeCalculationType.getId().intValue() > 1) {
this.amountOrPercentage = this.percentage;
} else {
this.amountOrPercentage = amount;
}
this.chargeOptions = chargeOptions;
}
}
@@ -502,13 +502,13 @@ public class ApiDataConversionServiceImpl implements ApiDataConversionService {
}
Set<String> chargeModifiedParameters = new HashSet<String>();
Long id = extractLongParameter("id", chargeRequestMap, chargeModifiedParameters);
Long chargeId = extractLongParameter("chargeId", chargeRequestMap, chargeModifiedParameters);
BigDecimal amount = extractBigDecimalParameter("amount", chargeRequestMap, chargeModifiedParameters);
Integer chargeTimeType = extractIntegerParameter("chargeTimeType", chargeRequestMap, chargeModifiedParameters);
Integer chargeCalculationType = extractIntegerParameter("chargeCalculationType", chargeRequestMap, chargeModifiedParameters);
charges[i] = new LoanChargeCommand(chargeModifiedParameters, null, null,chargeId, amount,
chargeTimeType, chargeCalculationType);
charges[i] = new LoanChargeCommand(chargeModifiedParameters, id, null,chargeId, amount, chargeTimeType, chargeCalculationType);
}
}
}
@@ -209,19 +209,17 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
for (LoanCharge charge : this.charges) {
if (charge.isDueAtDisbursement()) {
chargesDue = chargesDue.plus(charge.calculateMonetaryAmount());
chargesDue = chargesDue.plus(charge.amount());
}
}
return chargesDue.getAmount();
}
private Set<LoanCharge> associateChargesWithThisLoan(Set<LoanCharge> loanCharges) {
private Set<LoanCharge> associateChargesWithThisLoan(final Set<LoanCharge> loanCharges) {
for (LoanCharge loanCharge : loanCharges) {
loanCharge.update(this);
}
return loanCharges;
}
@@ -16,7 +16,6 @@ import org.mifosng.platform.api.commands.LoanChargeCommand;
import org.mifosng.platform.charge.domain.Charge;
import org.mifosng.platform.charge.domain.ChargeCalculationType;
import org.mifosng.platform.charge.domain.ChargeTimeType;
import org.mifosng.platform.currency.domain.Money;
import org.springframework.data.jpa.domain.AbstractPersistable;
@Entity
@@ -184,11 +183,7 @@ public class LoanCharge extends AbstractPersistable<Long> {
this.loan = loan;
}
public void update(final LoanChargeCommand command){
if (command.isAmountChanged()) {
this.amount = command.getAmount();
}
public void update(final LoanChargeCommand command, final BigDecimal loanPrincipal){
if (command.isChargeTimeTypeChanged()){
this.chargeTime = ChargeTimeType.fromInt(command.getChargeTimeType()).getValue();
@@ -197,51 +192,79 @@ public class LoanCharge extends AbstractPersistable<Long> {
if (command.isChargeCalculationTypeChanged()){
this.chargeCalculation = ChargeCalculationType.fromInt(command.getChargeCalculationType()).getValue();
}
if (command.isAmountChanged()) {
switch (ChargeCalculationType.fromInt(this.chargeCalculation)) {
case INVALID:
break;
case FLAT:
this.amount = command.getAmount();
break;
case PERCENT_OF_AMOUNT:
this.percentage = command.getAmount();
this.amountPercentageAppliedTo = loanPrincipal;
this.amount = percentageOf(this.amountPercentageAppliedTo, this.percentage);
this.amountOutstanding = calculateOutstanding();
break;
case PERCENT_OF_AMOUNT_AND_INTEREST:
this.percentage = command.getAmount();
this.amount = null;
this.amountPercentageAppliedTo = null;
this.amountOutstanding = null;
break;
case PERCENT_OF_INTEREST:
this.percentage = command.getAmount();
this.amount = null;
this.amountPercentageAppliedTo = null;
this.amountOutstanding = null;
break;
}
}
}
public boolean isDueAtDisbursement() {
return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.DISBURSEMENT);
}
public BigDecimal calculateMonetaryAmount(final Money principalDisbursed) {
BigDecimal calculatedAmount = BigDecimal.ZERO;
switch (ChargeCalculationType.fromInt(this.chargeCalculation)) {
case INVALID:
calculatedAmount = this.amount;
break;
case FLAT:
calculatedAmount = this.amount;
break;
case PERCENT_OF_AMOUNT:
this.amountPercentageAppliedTo = principalDisbursed.getAmount();
this.amount = percentageOf(this.amountPercentageAppliedTo, this.percentage);
this.amountPaid = null;
this.amountOutstanding = calculateOutstanding();
calculatedAmount = this.amount;
break;
case PERCENT_OF_AMOUNT_AND_INTEREST:
this.percentage = null;
this.amount = null;
this.amountPercentageAppliedTo = null;
this.amountPaid = null;
this.amountOutstanding = null;
break;
case PERCENT_OF_INTEREST:
this.percentage = null;
this.amount = null;
this.amountPercentageAppliedTo = null;
this.amountPaid = null;
this.amountOutstanding = null;
break;
}
return calculatedAmount;
}
public BigDecimal calculateMonetaryAmount() {
return calculateMonetaryAmount(this.loan.getPrincpal());
}
// public BigDecimal calculateMonetaryAmount(final Money principalDisbursed) {
// BigDecimal calculatedAmount = BigDecimal.ZERO;
//
// switch (ChargeCalculationType.fromInt(this.chargeCalculation)) {
// case INVALID:
// calculatedAmount = this.amount;
// break;
// case FLAT:
// calculatedAmount = this.amount;
// break;
// case PERCENT_OF_AMOUNT:
// this.amountPercentageAppliedTo = principalDisbursed.getAmount();
// this.amount = percentageOf(this.amountPercentageAppliedTo, this.percentage);
// this.amountPaid = null;
// this.amountOutstanding = calculateOutstanding();
// calculatedAmount = this.amount;
// break;
// case PERCENT_OF_AMOUNT_AND_INTEREST:
// this.percentage = null;
// this.amount = null;
// this.amountPercentageAppliedTo = null;
// this.amountPaid = null;
// this.amountOutstanding = null;
// break;
// case PERCENT_OF_INTEREST:
// this.percentage = null;
// this.amount = null;
// this.amountPercentageAppliedTo = null;
// this.amountPaid = null;
// this.amountOutstanding = null;
// break;
// }
//
// return calculatedAmount;
// }
//
// public BigDecimal calculateMonetaryAmount() {
// return calculateMonetaryAmount(this.loan.getPrincpal());
// }
private boolean isGreaterThanZero(final BigDecimal value) {
return value.compareTo(BigDecimal.ZERO) == 1;
@@ -291,4 +314,8 @@ public class LoanCharge extends AbstractPersistable<Long> {
this.amountOutstanding = this.amount;
this.paid = false;
}
public BigDecimal amount() {
return this.amount;
}
}
@@ -2,24 +2,17 @@ package org.mifosng.platform.loan.service;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.mifosng.platform.api.LoanScheduleNewData;
import org.mifosng.platform.api.commands.LoanApplicationCommand;
import org.mifosng.platform.api.commands.LoanChargeCommand;
import org.mifosng.platform.api.data.LoanSchedulePeriodData;
import org.mifosng.platform.charge.domain.Charge;
import org.mifosng.platform.charge.domain.ChargeRepository;
import org.mifosng.platform.client.domain.Client;
import org.mifosng.platform.client.domain.ClientRepository;
import org.mifosng.platform.currency.domain.MonetaryCurrency;
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;
@@ -46,7 +39,6 @@ import org.mifosng.platform.staff.domain.Staff;
import org.mifosng.platform.staff.domain.StaffRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
@Service
public class LoanAssembler {
@@ -55,27 +47,27 @@ public class LoanAssembler {
private final ClientRepository clientRepository;
private final AprCalculator aprCalculator = new AprCalculator();
private final FundRepository fundRepository;
private final ChargeRepository chargeRepository;
private final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository;
private final StaffRepository staffRepository;
private final CalculationPlatformService calculationPlatformService;
private final LoanChargeAssembler loanChargeAssembler;
@Autowired
public LoanAssembler(
final LoanProductRepository loanProductRepository,
final ClientRepository clientRepository,
final FundRepository fundRepository,
final ChargeRepository chargeRepository,
final LoanTransactionProcessingStrategyRepository loanTransactionProcessingStrategyRepository,
final StaffRepository staffRepository,
final CalculationPlatformService calculationPlatformService) {
final CalculationPlatformService calculationPlatformService,
final LoanChargeAssembler loanChargeAssembler) {
this.loanProductRepository = loanProductRepository;
this.clientRepository = clientRepository;
this.fundRepository = fundRepository;
this.chargeRepository = chargeRepository;
this.loanTransactionProcessingStrategyRepository = loanTransactionProcessingStrategyRepository;
this.staffRepository = staffRepository;
this.calculationPlatformService = calculationPlatformService;
this.loanChargeAssembler = loanChargeAssembler;
}
public Loan assembleFrom(final LoanApplicationCommand command) {
@@ -104,7 +96,7 @@ public class LoanAssembler {
Staff loanOfficer= findLoanOfficerByIdIfProvided(command.getLoanOfficerId());
// 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(), loanProduct.getCurrency().getCode(), command.getPrincipal());
final Set<LoanCharge> loanCharges = this.loanChargeAssembler.assembleFrom(command.getCharges(), loanProduct.getCharges(), command.getPrincipal());
Loan loan = Loan.createNew(fund,loanOfficer, loanTransactionProcessingStrategy, loanProduct, client, loanRepaymentScheduleDetail, loanCharges);
loan.setExternalId(command.getExternalId());
@@ -130,42 +122,6 @@ public class LoanAssembler {
return loan;
}
public Set<LoanCharge> assembleSetOfLoanCharges(final LoanChargeCommand[] chargesPassedAtCreation,
final Set<Charge> chargesInheritedFromProduct,
final String loanCurrencyCode,
final BigDecimal loanPrincipal) {
Set<LoanCharge> loanCharges = new HashSet<LoanCharge>();
if (!ObjectUtils.isEmpty(chargesPassedAtCreation)) {
for (LoanChargeCommand loanChargeCommand : chargesPassedAtCreation) {
Long chargeDefinitionId = loanChargeCommand.getChargeId();
Charge chargeDefinition = this.chargeRepository.findOne(chargeDefinitionId);
if (chargeDefinition == null || chargeDefinition.isDeleted()) {
throw new ChargeNotFoundException(chargeDefinitionId);
}
if (!chargeDefinition.isActive()) {
throw new ChargeIsNotActiveException(chargeDefinitionId);
}
if (!loanCurrencyCode.equals(chargeDefinition.getCurrencyCode())){
String errorMessage = "Charge and Loan must have the same currency.";
throw new InvalidCurrencyException("charge", "attach.to.loan", errorMessage);
}
loanCharges.add(LoanCharge.createNewWithoutLoan(chargeDefinition, loanChargeCommand, loanPrincipal));
}
} 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());
@@ -7,6 +7,7 @@ import java.util.Set;
import org.mifosng.platform.api.commands.LoanChargeCommand;
import org.mifosng.platform.charge.domain.Charge;
import org.mifosng.platform.charge.domain.ChargeRepository;
import org.mifosng.platform.exceptions.ChargeNotFoundException;
import org.mifosng.platform.loan.domain.LoanCharge;
import org.mifosng.platform.loan.domain.LoanChargeRepository;
import org.springframework.beans.factory.annotation.Autowired;
@@ -15,7 +16,7 @@ import org.springframework.stereotype.Service;
@Service
public class LoanChargeAssembler {
private ChargeRepository chargeRepository;
private final ChargeRepository chargeRepository;
private final LoanChargeRepository loanChargeRepository;
@Autowired
@@ -30,13 +31,23 @@ public class LoanChargeAssembler {
if (charges != null) {
for (LoanChargeCommand loanChargeCommand : charges) {
if (loanChargeCommand.getId() == null) {
final Charge chargeDefinition = this.chargeRepository.findOne(loanChargeCommand.getChargeId());
final Long chargeDefinitionId = loanChargeCommand.getChargeId();
final Charge chargeDefinition = this.chargeRepository.findOne(chargeDefinitionId);
if (chargeDefinition == null || chargeDefinition.isDeleted()) {
throw new ChargeNotFoundException(chargeDefinitionId);
}
final LoanCharge loanCharge = LoanCharge.createNewWithoutLoan(chargeDefinition, loanChargeCommand, loanPrincipal);
loanCharges.add(loanCharge);
} else {
final LoanCharge loanCharge = this.loanChargeRepository.findOne(loanChargeCommand.getId());
final Long loanChargeId = loanChargeCommand.getId();
final LoanCharge loanCharge = this.loanChargeRepository.findOne(loanChargeId);
if (loanCharge == null) {
throw new ChargeNotFoundException(loanChargeId);
}
loanCharge.update(loanChargeCommand, loanPrincipal);
loanCharges.add(loanCharge);
}
}
@@ -44,4 +55,19 @@ public class LoanChargeAssembler {
return loanCharges;
}
public Set<LoanCharge> assembleFrom(final LoanChargeCommand[] charges, final Set<Charge> chargesInheritedFromProduct, final BigDecimal loanPrincipal) {
Set<LoanCharge> loanCharges = new HashSet<LoanCharge>();
if (charges != null) {
loanCharges = assembleFrom(charges, loanPrincipal);
} else {
for (Charge productCharge : chargesInheritedFromProduct) {
loanCharges.add(LoanCharge.createNew(productCharge));
}
}
return loanCharges;
}
}
@@ -70,15 +70,18 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final LoanProductRepository loanProductRepository;
private final ChargeRepository chargeRepository;
private final LoanChargeRepository loanChargeRepository;
private final LoanChargeAssembler loanChargeAssembler;
@Autowired
public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context, final LoanAssembler loanAssembler,
public LoanWritePlatformServiceJpaRepositoryImpl(final PlatformSecurityContext context,
final LoanAssembler loanAssembler, final LoanChargeAssembler loanChargeAssembler,
final LoanRepository loanRepository, final LoanTransactionRepository loanTransactionRepository,
final NoteRepository noteRepository, final CalculationPlatformService calculationPlatformService,
final ClientRepository clientRepository, final LoanProductRepository loanProductRepository,
final ChargeRepository chargeRepository, final LoanChargeRepository loanChargeRepository) {
this.context = context;
this.loanAssembler = loanAssembler;
this.loanChargeAssembler = loanChargeAssembler;
this.loanRepository = loanRepository;
this.loanTransactionRepository = loanTransactionRepository;
this.noteRepository = noteRepository;
@@ -133,30 +136,30 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
validator.validate();
// TODO - fix up permissions for loan modification
LocalDate submittedOn = command.getSubmittedOnDate();
final LocalDate submittedOn = command.getSubmittedOnDate();
if (this.isBeforeToday(submittedOn) && currentUser.hasNotPermissionForAnyOf("CAN_SUBMIT_HISTORIC_LOAN_APPLICATION_ROLE", "PORTFOLIO_MANAGEMENT_SUPER_USER_ROLE")) {
throw new NoAuthorizationException("Cannot modify backdated loan.");
}
Loan loan = this.loanRepository.findOne(command.getLoanId());
final Loan loan = this.loanRepository.findOne(command.getLoanId());
if (loan == null) {
throw new LoanNotFoundException(command.getLoanId());
}
LoanProduct loanProduct = this.loanProductRepository.findOne(command.getProductId());
final LoanProduct loanProduct = this.loanProductRepository.findOne(command.getProductId());
if (loanProduct == null) {
throw new LoanProductNotFoundException(command.getProductId());
}
Client client = this.clientRepository.findOne(command.getClientId());
final Client client = this.clientRepository.findOne(command.getClientId());
if (client == null || client.isDeleted()) {
throw new ClientNotFoundException(command.getClientId());
}
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.assembleSetOfLoanCharges(command.getCharges(), loanProduct.getCharges(), loan.getCurrency().getCode(), loan.getPrincpal().getAmount());
final Fund fund = this.loanAssembler.findFundByIdIfProvided(command.getFundId());
final Staff loanOfficer = this.loanAssembler.findLoanOfficerByIdIfProvided(command.getLoanOfficerId());
final LoanTransactionProcessingStrategy strategy = this.loanAssembler.findStrategyByIdIfProvided(command.getTransactionProcessingStrategyId());
final Set<LoanCharge> charges = this.loanChargeAssembler.assembleFrom(command.getCharges(), loanProduct.getCharges(), loan.getPrincpal().getAmount());
final LoanScheduleNewData loanSchedule = this.calculationPlatformService.calculateLoanScheduleNew(command.toCalculateLoanScheduleCommand());
loan.modifyLoanApplication(command, client, loanProduct, fund, strategy, loanSchedule, charges, loanOfficer);
@@ -584,7 +587,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
@Override
public EntityIdentifier updateLoanCharge(LoanChargeCommand command) {
public EntityIdentifier updateLoanCharge(final LoanChargeCommand command) {
this.context.authenticatedUser();
@@ -598,7 +601,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
for (LoanCharge loanCharge : loan.getCharges()){
if (loanCharge.getId().equals(command.getId())){
loanCharge.update(command);
loanCharge.update(command, loan.getPrincpal().getAmount());
loan.updateTotalChargesDueAtDisbursement();
this.loanRepository.saveAndFlush(loan);
return new EntityIdentifier(loanCharge.getId());
@@ -62,7 +62,7 @@ public class EqualInstallmentsAmortizationLoanScheduleGenerator implements Amort
for (LoanCharge loanCharge : loanCharges) {
// FIXME - KW - right now only charges at disbursement are supported.
if (loanCharge.isDueAtDisbursement()) {
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.calculateMonetaryAmount(principalDisbursed));
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount());
}
}
@@ -52,7 +52,7 @@ public class EqualPrincipalAmortizationLoanScheduleGenerator implements Amortiza
for (LoanCharge loanCharge : loanCharges) {
// FIXME - KW - right now only charges at disbursement are supported.
if (loanCharge.isDueAtDisbursement()) {
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.calculateMonetaryAmount(principalDisbursed));
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount());
}
}
@@ -67,7 +67,7 @@ public class FlatMethodLoanScheduleGenerator implements LoanScheduleGenerator {
for (LoanCharge loanCharge : loanCharges) {
// FIXME - KW - right now only charges at disbursement are supported.
if (loanCharge.isDueAtDisbursement()) {
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.calculateMonetaryAmount(principalDisbursed));
chargesDueAtTimeOfDisbursement = chargesDueAtTimeOfDisbursement.add(loanCharge.amount());
}
}