MIFOSX-90: cater for some validation and edge cases around loan charges and loans.

This commit is contained in:
Keith Woodlock
2012-11-02 15:17:46 +00:00
parent ee63559abd
commit db3f0401d9
11 changed files with 160 additions and 110 deletions
@@ -1,24 +0,0 @@
package org.mifosng.platform;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.mifosng.platform.client.domain.Client;
import org.mifosng.platform.loan.domain.Loan;
import org.springframework.data.jpa.domain.Specification;
@SuppressWarnings("unused")
public final class Specifications {
public static Specification<Loan> loansThatMatch(final Client client) {
return new Specification<Loan>() {
@Override
public Predicate toPredicate(final Root<Loan> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
return cb.equal(root.get("client"), client);
}
};
}
}
@@ -2,7 +2,6 @@ package org.mifosng.platform.api;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.Consumes;
@@ -289,13 +288,7 @@ public class LoansApiResource {
final LoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanSchedule(command);
final Set<String> typicalLoanScheduleResponseParameters = new HashSet<String>(
Arrays.asList("periods", "cumulativePrincipalDisbursed"));
Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
if (responseParameters.isEmpty()) {
responseParameters.addAll(typicalLoanScheduleResponseParameters);
}
final Set<String> responseParameters = ApiParameterHelper.extractFieldsForResponseIfProvided(uriInfo.getQueryParameters());
boolean prettyPrint = ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters());
return this.apiJsonSerializerService.serializeLoanScheduleDataToJson(prettyPrint, responseParameters, loanSchedule);
@@ -252,7 +252,7 @@ public class LoanApplicationCommand {
return this.modifiedParameters.contains("submittedOnDate");
}
public boolean isExpectedDisbursementDateChanged() {
public boolean isExpectedDisbursementDatePassed() {
return this.modifiedParameters.contains("expectedDisbursementDate");
}
@@ -557,16 +557,16 @@ public class ApiDataConversionServiceImpl implements ApiDataConversionService {
for (int i=0; i<array.size(); i++) {
final JsonObject loanChargeElement = array.get(i).getAsJsonObject();
final Set<String> chargeModifiedParameters = new HashSet<String>();
final Set<String> parametersPassedInForChargesCommand = new HashSet<String>();
final Long id = helper.extractLongNamed("id", loanChargeElement, chargeModifiedParameters);
final Long chargeId = helper.extractLongNamed("chargeId", loanChargeElement, chargeModifiedParameters);
final BigDecimal amount = helper.extractBigDecimalNamed("amount", loanChargeElement, locale, chargeModifiedParameters);
final Integer chargeTimeType = helper.extractIntegerNamed("chargeTimeType", loanChargeElement, locale, chargeModifiedParameters);
final Integer chargeCalculationType = helper.extractIntegerNamed("chargeCalculationType", loanChargeElement, locale, chargeModifiedParameters);
final LocalDate specifiedDueDate = helper.extractLocalDateNamed("specifiedDueDate", loanChargeElement, dateFormat, locale, parametersPassedInCommand);
final Long id = helper.extractLongNamed("id", loanChargeElement, parametersPassedInForChargesCommand);
final Long chargeId = helper.extractLongNamed("chargeId", loanChargeElement, parametersPassedInForChargesCommand);
final BigDecimal amount = helper.extractBigDecimalNamed("amount", loanChargeElement, locale, parametersPassedInForChargesCommand);
final Integer chargeTimeType = helper.extractIntegerNamed("chargeTimeType", loanChargeElement, locale, parametersPassedInForChargesCommand);
final Integer chargeCalculationType = helper.extractIntegerNamed("chargeCalculationType", loanChargeElement, locale, parametersPassedInForChargesCommand);
final LocalDate specifiedDueDate = helper.extractLocalDateNamed("specifiedDueDate", loanChargeElement, dateFormat, locale, parametersPassedInForChargesCommand);
charges[i] = new LoanChargeCommand(chargeModifiedParameters, id, null, chargeId, amount, chargeTimeType, chargeCalculationType, specifiedDueDate);
charges[i] = new LoanChargeCommand(parametersPassedInForChargesCommand, id, null, chargeId, amount, chargeTimeType, chargeCalculationType, specifiedDueDate);
}
}
}
@@ -0,0 +1,12 @@
package org.mifosng.platform.exceptions;
/**
* {@link AbstractPlatformDomainRuleException} thrown when loan charge is not allowed to be added to the loan.
*/
public class LoanChargeCannotBeAddedException extends AbstractPlatformDomainRuleException {
public LoanChargeCannotBeAddedException(final String entity, final String postFix, final String defaultUserMessage, final Object... defaultUserMessageArgs) {
super("error.msg."+entity+".cannot.be.added.as." + postFix, defaultUserMessage, defaultUserMessageArgs);
}
}
@@ -0,0 +1,15 @@
package org.mifosng.platform.exceptions;
/**
* {@link AbstractPlatformDomainRuleException} thrown when loan charge does not exist.
*/
public class LoanChargeNotFoundException extends AbstractPlatformResourceNotFoundException {
public LoanChargeNotFoundException(final Long id) {
super("error.msg.loanCharge.id.invalid", "Loan charge with identifier " + id + " does not exist", id);
}
public LoanChargeNotFoundException(final Long id, final Long loanId) {
super("error.msg.loanCharge.id.invalid.for.given.loan", "Loan charge with identifier " + id + " does not exist for loan " + loanId, id, loanId);
}
}
@@ -35,6 +35,7 @@ import org.mifosng.platform.currency.domain.MonetaryCurrency;
import org.mifosng.platform.currency.domain.Money;
import org.mifosng.platform.exceptions.InvalidLoanStateTransitionException;
import org.mifosng.platform.exceptions.InvalidLoanTransactionTypeException;
import org.mifosng.platform.exceptions.LoanChargeCannotBeAddedException;
import org.mifosng.platform.fund.domain.Fund;
import org.mifosng.platform.group.domain.Group;
import org.mifosng.platform.infrastructure.AbstractAuditableCustom;
@@ -259,6 +260,23 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
public void addLoanCharge(final LoanCharge loanCharge) {
if (isDisbursed() && loanCharge.isDueAtDisbursement()) {
// Note: added this constraint to restrict adding charges to a loan after it is disbursed
// if the loan charge payment type is 'Disbursement'.
// To undo this constraint would mean resolving how charges due are disbursement are handled at present.
// When a loan is disbursed and has charges due at disbursement, a transaction is created to auto record
// payment of the charges (user has no choice in saying they were or werent paid) - so its assumed they were paid.
final String defaultUserMessage = "This charge which is due at disbursement cannot be added as the loan is already disbursed.";
throw new LoanChargeCannotBeAddedException("loanCharge", "due.at.disbursement.and.loan.is.disbursed", defaultUserMessage, getId(), loanCharge.name());
}
if (loanCharge.isSpecifiedDueDate() && !loanCharge.isDueForCollectionBetween(getDisbursementDate(), getLastRepaymentPeriodDueDate())) {
final String defaultUserMessage = "This charge which is due at disbursement cannot be added as the loan is already disbursed.";
throw new LoanChargeCannotBeAddedException("loanCharge", "specified.due.date.outside.range", defaultUserMessage, getDisbursementDate(), getLastRepaymentPeriodDueDate(), loanCharge.name());
}
loanCharge.update(this);
this.charges.add(loanCharge);
@@ -276,6 +294,10 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
}
}
private LocalDate getLastRepaymentPeriodDueDate() {
return this.repaymentScheduleInstallments.get(this.repaymentScheduleInstallments.size()-1).getDueDate();
}
public void removeLoanCharge(final LoanCharge loanCharge) {
boolean removed = this.charges.remove(loanCharge);
if (removed) {
@@ -286,6 +308,32 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
LoanScheduleWrapper wrapper = new LoanScheduleWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), this.repaymentScheduleInstallments, this.charges);
// if removing disbursement charge - accomodate this on the transaction that tracked its payment
if (loanCharge.isDueAtDisbursement()) {
LoanTransaction transactionToRemove = null;
for (LoanTransaction transaction : this.loanTransactions) {
if (transaction.isRepaymentAtDisbursement()) {
final MonetaryCurrency currency = loanCurrency();
final Money chargeAmount = Money.of(currency, loanCharge.amount());
if (transaction.isGreaterThan(chargeAmount)) {
final Money principalPortion = Money.zero(currency);
final Money interestPortion = Money.zero(currency);
final Money penaltychargesPortion = Money.zero(currency);
final Money feeChargesPortion = chargeAmount;
transaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion);
} else {
transactionToRemove = transaction;
}
}
}
if (transactionToRemove != null) {
this.loanTransactions.remove(transactionToRemove);
}
}
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessor.determineProcessor(this.transactionProcessingStrategy);
if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loanCurrency())) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
@@ -318,6 +366,8 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
final Staff loanOfficer,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
// FIXME - KW - whilst the methods are named isXXXChanged, this really only is asking has the parameter for that field been passed in request,
// an additional check to see if the value passed is different to the current value may bee need for all these like it is for expectedDisbursementDate
if (command.isClientChanged()) {
this.client = client;
}
@@ -346,13 +396,11 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
this.submittedOnDate = command.getSubmittedOnDate().toDate();
}
if (command.isExpectedDisbursementDateChanged()) {
if (command.getExpectedDisbursementDate() != null) {
if (command.isExpectedDisbursementDatePassed()) {
if (dateHasChanged(getExpectedDisbursedOnLocalDate(), command.getExpectedDisbursementDate())) {
this.expectedDisbursedOnDate = command.getExpectedDisbursementDate().toDate();
removeFirstDisbursementTransaction();
disburse(command.getExpectedDisbursementDate(), loanLifecycleStateMachine, false);
} else {
this.expectedDisbursedOnDate = null;
}
}
@@ -406,6 +454,19 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
loanRepaymentScheduleTransactionProcessor.handleTransaction(repaymentsOrWaivers, getCurrency(), this.repaymentScheduleInstallments, this.charges);
}
private boolean dateHasChanged(final LocalDate originalLocalDate, final LocalDate providedLocalDate) {
boolean dateHasChanged = false;
if (originalLocalDate != null) {
dateHasChanged = originalLocalDate.equals(providedLocalDate);
} else {
dateHasChanged = (providedLocalDate != null);
}
return dateHasChanged;
}
private void removeFirstDisbursementTransaction() {
for (LoanTransaction loanTransaction : loanTransactions) {
if (loanTransaction.isDisbursement()) {
@@ -564,10 +625,10 @@ public class Loan extends AbstractAuditableCustom<AppUser, Long> {
public void disburse(final LocalDate disbursedOn, final LoanLifecycleStateMachine loanLifecycleStateMachine, final boolean statusTransition) {
// ensure loan is in pre-disbursal state
updateLoanToPreDisbursalState();
if (statusTransition) {
// ensure loan is in pre-disbursal state
updateLoanToPreDisbursalState();
LoanStatus statusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
}
@@ -231,6 +231,10 @@ public class LoanCharge extends AbstractPersistable<Long> {
this.chargeCalculation = ChargeCalculationType.fromInt(command.getChargeCalculationType()).getValue();
}
if (command.isSpecifiedDueDateChanged()) {
this.dueForCollectionAsOfDate = command.getSpecifiedDueDate().toDate();
}
if (command.isAmountChanged()) {
switch (ChargeCalculationType.fromInt(this.chargeCalculation)) {
case INVALID:
@@ -263,6 +267,10 @@ public class LoanCharge extends AbstractPersistable<Long> {
public boolean isDueAtDisbursement() {
return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.DISBURSEMENT);
}
public boolean isSpecifiedDueDate() {
return ChargeTimeType.fromInt(this.chargeTime).equals(ChargeTimeType.SPECIFIED_DUE_DATE);
}
private boolean isGreaterThanZero(final BigDecimal value) {
return value.compareTo(BigDecimal.ZERO) == 1;
@@ -276,7 +284,7 @@ public class LoanCharge extends AbstractPersistable<Long> {
this.amount, this.chargeTime, this.chargeCalculation, specifiedDueDate);
}
private LocalDate getDueForCollectionAsOfLocalDate() {
public LocalDate getDueForCollectionAsOfLocalDate() {
LocalDate specifiedDueDate = null;
if (this.dueForCollectionAsOfDate != null) {
specifiedDueDate = new LocalDate(this.dueForCollectionAsOfDate);
@@ -390,4 +398,8 @@ public class LoanCharge extends AbstractPersistable<Long> {
return incrementBy.minus(amountPaidOnThisCharge);
}
public String name() {
return this.charge.getName();
}
}
@@ -224,6 +224,10 @@ public class LoanTransaction extends AbstractAuditableCustom<AppUser, Long> {
this.feeChargesPortion = null;
}
public boolean isGreaterThan(final Money monetaryAmount) {
return getAmount(monetaryAmount.getCurrency()).isGreaterThan(monetaryAmount);
}
public boolean isGreaterThanZero(final MonetaryCurrency currency) {
return getAmount(currency).isGreaterThanZero();
}
@@ -9,6 +9,7 @@ import org.mifosng.platform.charge.domain.Charge;
import org.mifosng.platform.charge.domain.ChargeRepository;
import org.mifosng.platform.exceptions.ChargeIsNotActiveException;
import org.mifosng.platform.exceptions.ChargeNotFoundException;
import org.mifosng.platform.exceptions.LoanChargeNotFoundException;
import org.mifosng.platform.loan.domain.LoanCharge;
import org.mifosng.platform.loan.domain.LoanChargeRepository;
import org.springframework.beans.factory.annotation.Autowired;
@@ -48,7 +49,7 @@ public class LoanChargeAssembler {
final Long loanChargeId = loanChargeCommand.getId();
final LoanCharge loanCharge = this.loanChargeRepository.findOne(loanChargeId);
if (loanCharge == null) {
throw new ChargeNotFoundException(loanChargeId);
throw new LoanChargeNotFoundException(loanChargeId);
}
loanCharge.update(loanChargeCommand, loanPrincipal);
@@ -31,6 +31,7 @@ import org.mifosng.platform.exceptions.ChargeIsNotActiveException;
import org.mifosng.platform.exceptions.ChargeNotFoundException;
import org.mifosng.platform.exceptions.ClientNotFoundException;
import org.mifosng.platform.exceptions.InvalidCurrencyException;
import org.mifosng.platform.exceptions.LoanChargeNotFoundException;
import org.mifosng.platform.exceptions.LoanNotFoundException;
import org.mifosng.platform.exceptions.LoanNotInSubmittedAndPendingApprovalStateCannotBeDeleted;
import org.mifosng.platform.exceptions.LoanOfficerAssignmentException;
@@ -145,10 +146,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
throw new NoAuthorizationException("Cannot modify backdated loan.");
}
final Loan loan = this.loanRepository.findOne(command.getLoanId());
if (loan == null) {
throw new LoanNotFoundException(command.getLoanId());
}
final Loan loan = retrieveLoanBy(command.getLoanId());
final LoanProduct loanProduct = this.loanProductRepository.findOne(command.getProductId());
if (loanProduct == null) {
@@ -166,6 +164,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Set<LoanCharge> charges = this.loanChargeAssembler.assembleFrom(command.getCharges(), loanProduct.getCharges(), loan.getPrincpal().getAmount());
final LoanScheduleData loanSchedule = this.calculationPlatformService.calculateLoanSchedule(command.toCalculateLoanScheduleCommand());
// FIXME - kw - restrict ability to modify loan to modifying the 'loan application' stage - once it has been disbursed, should be allowed to use this feature
// - other facilities are in place such as 'undo disbursal'
loan.modifyLoanApplication(command, client, loanProduct, fund, strategy, loanSchedule, charges, loanOfficer, defaultLoanLifecycleStateMachine());
this.loanRepository.save(loan);
@@ -184,10 +185,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
context.authenticatedUser();
Loan loan = this.loanRepository.findOne(loanId);
if (loan == null) {
throw new LoanNotFoundException(loanId);
}
final Loan loan = retrieveLoanBy(loanId);
if (loan.isNotSubmittedAndPendingApproval()) {
throw new LoanNotInSubmittedAndPendingApprovalStateCannotBeDeleted(loanId);
@@ -210,10 +208,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
LoanStateTransitionCommandValidator validator = new LoanStateTransitionCommandValidator(command);
validator.validate();
Loan loan = this.loanRepository.findOne(command.getLoanId());
if (loan == null) {
throw new LoanNotFoundException(command.getLoanId());
}
final Loan loan = retrieveLoanBy(command.getLoanId());
LocalDate eventDate = command.getEventDate();
if (this.isBeforeToday(eventDate) && currentUser.canNotApproveLoanInPast()) {
@@ -238,10 +233,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
context.authenticatedUser();
Loan loan = this.loanRepository.findOne(command.getLoanId());
if (loan == null) {
throw new LoanNotFoundException(command.getLoanId());
}
final Loan loan = retrieveLoanBy(command.getLoanId());
loan.undoApproval(defaultLoanLifecycleStateMachine());
this.loanRepository.save(loan);
@@ -264,10 +256,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
LoanStateTransitionCommandValidator validator = new LoanStateTransitionCommandValidator(command);
validator.validate();
Loan loan = this.loanRepository.findOne(command.getLoanId());
if (loan == null) {
throw new LoanNotFoundException(command.getLoanId());
}
final Loan loan = retrieveLoanBy(command.getLoanId());
LocalDate eventDate = command.getEventDate();
if (this.isBeforeToday(eventDate) && currentUser.canNotRejectLoanInPast()) {
@@ -295,10 +284,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
LoanStateTransitionCommandValidator validator = new LoanStateTransitionCommandValidator(command);
validator.validate();
Loan loan = this.loanRepository.findOne(command.getLoanId());
if (loan == null) {
throw new LoanNotFoundException(command.getLoanId());
}
final Loan loan = retrieveLoanBy(command.getLoanId());
LocalDate eventDate = command.getEventDate();
if (this.isBeforeToday(eventDate) && currentUser.canNotWithdrawByClientLoanInPast()) {
@@ -326,10 +312,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
LoanStateTransitionCommandValidator validator = new LoanStateTransitionCommandValidator(command);
validator.validate();
Loan loan = this.loanRepository.findOne(command.getLoanId());
if (loan == null) {
throw new LoanNotFoundException(command.getLoanId());
}
final Loan loan = retrieveLoanBy(command.getLoanId());
String noteText = command.getNote();
LocalDate actualDisbursementDate = command.getEventDate();
@@ -673,22 +656,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
validator.validateForUpdate();
final Long loanId = command.getLoanId();
final Loan loan = this.loanRepository.findOne(loanId);
if (loan == null) {
throw new LoanNotFoundException(loanId);
}
final Loan loan = retrieveLoanBy(loanId);
final Long loanChargeId = command.getId();
final LoanCharge loanCharge = this.loanChargeRepository.findOne(loanChargeId);
if (loanCharge == null) {
// FIXME - kw - should be loan charge not found, not charge not found.
throw new ChargeNotFoundException(loanChargeId);
}
if (loanCharge.hasNotLoanIdentifiedBy(loanId)) {
// FIXME - kw - should be loan charge not found, not charge not found.
throw new ChargeNotFoundException(loanChargeId);
}
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
loan.updateLoanCharge(loanCharge, command);
this.loanRepository.saveAndFlush(loan);
@@ -702,27 +673,35 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
this.context.authenticatedUser();
final Loan loan = this.loanRepository.findOne(loanId);
if (loan == null) {
throw new LoanNotFoundException(loanId);
}
final Loan loan = retrieveLoanBy(loanId);
final LoanCharge loanCharge = this.loanChargeRepository.findOne(loanChargeId);
if (loanCharge == null) {
// FIXME - kw - should be loan charge not found, not charge not found.
throw new ChargeNotFoundException(loanChargeId);
}
if (loanCharge.hasNotLoanIdentifiedBy(loanId)) {
// FIXME - kw - should be loan charge not found, not charge not found.
throw new ChargeNotFoundException(loanChargeId);
}
final LoanCharge loanCharge = retrieveLoanChargeBy(loanId, loanChargeId);
loan.removeLoanCharge(loanCharge);
this.loanRepository.saveAndFlush(loan);
return new EntityIdentifier(loanCharge.getId());
}
private Loan retrieveLoanBy(final Long loanId) {
final Loan loan = this.loanRepository.findOne(loanId);
if (loan == null) {
throw new LoanNotFoundException(loanId);
}
return loan;
}
private LoanCharge retrieveLoanChargeBy(final Long loanId, final Long loanChargeId) {
final LoanCharge loanCharge = this.loanChargeRepository.findOne(loanChargeId);
if (loanCharge == null) {
throw new LoanChargeNotFoundException(loanChargeId);
}
if (loanCharge.hasNotLoanIdentifiedBy(loanId)) {
throw new LoanChargeNotFoundException(loanChargeId, loanId);
}
return loanCharge;
}
@Transactional
@Override
@@ -739,10 +718,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
for (String loanIdString : command.getLoans()){
final Long loanId = Long.valueOf(loanIdString);
final Loan loan = this.loanRepository.findOne(loanId);
if (loan == null) {
throw new LoanNotFoundException(loanId);
}
final Loan loan = retrieveLoanBy(loanId);
if (!loan.hasLoanOfficer(fromLoanOfficer)){
throw new LoanOfficerAssignmentException(loan.getId(), fromLoanOfficer.getId());